Resources and pages
A panel shows two kinds of content: resources – lists, forms and detail pages of a model – and pages with content of their own.
resources()
Section titled “resources()”public function resources(): array{ return [InvoiceResource::class, CustomerResource::class, ...Access::resources()];}Every resource gets its routes (/admin/invoices, …/create,
…/{uuid}, …/{uuid}/edit), a navigation entry and its permissions in
the role editor. The slug of a resource must be unique within the panel.
The same resource class can be registered in several panels; it is instantiated per panel and builds its URLs from the panel. Code that needs “the resource of a model in this panel” asks the panel:
$panel = service('panels')->current();$panel->resourceForModel(CustomerModel::class); // first resource of the model, or null$panel->resourceByClass(CustomerResource::class); // InvalidArgumentException if not registered$panel->resource('customers'); // by slug, or nullSelects, relationship columns and counts use resourceForModel() to apply
the permissions and policies of the related records: registering a
resource for a model makes its records follow the rules of that resource
everywhere in the panel.
Access::resources() and Access::pages()
Section titled “Access::resources() and Access::pages()”use Anvil\Access\Access;
public function resources(): array { return [...$own, ...Access::resources()]; }public function pages(): array { return [Dashboard::class, ...Access::pages()]; }Access::resources() adds the user and role administration (and API
clients when the API is used), Access::pages() the
inbox, the profile and the
workflow settings. Leave them out in
panels that should not offer them, e.g. a portal gets only
Access::pages() for the profile and the inbox. Packages offer the same,
e.g. Sync::resources() and Sync::pages().
pages()
Section titled “pages()”public function pages(): array{ return [Dashboard::class, ReportsPage::class];}Defaults to [Dashboard::class]. A page renders its own content inside
the panel layout:
php spark make:anvil-page Reports --suffix # App\Pages\ReportsPage + app/Views/pages/reports.phpuse Anvil\Page\Page;
final class ReportsPage extends Page{ public function title(): string { return 'Reports'; }
public function subtitle(): ?string { return 'Revenue of the current year'; }
public function navigationIcon(): string { return 'bar-chart'; }
public function navigationGroup(): ?string { return 'Reports'; }
public function content(): string { return view('reports/index', ['totals' => model(InvoiceModel::class)->totals()]); }}| Method | Default | Purpose |
|---|---|---|
title() |
– (required) | heading, browser title and breadcrumb |
content() |
– (required) | HTML of the page body; escape every value |
subtitle() |
null |
line below the heading |
slug() |
class name in kebab case without Page (ReportsPage → reports) |
URI segment, /admin/reports |
permission() |
<panel>.page.<slug> |
permission to open the page |
breadcrumbs() |
[url() => title()] |
the way after the dashboard, url => label; pages with sub views add their levels, see below |
url() |
the URL of the page |
The navigation methods (navigationLabel(), navigationIcon(),
navigationGroup(), navigationSort(), navigationBadge(),
navigationBadgeColor(), shouldRegisterNavigation()) are described in
Navigation.
Sub views
Section titled “Sub views”A page can show one entry of its own list, e.g. the settings of one
workflow at /admin/workflows?workflow=order-approval. Build it like the
detail page of a record: the entry as title, the page as subtitle, and a
breadcrumb with both levels.
public function title(): string{ return $this->selected()?->title() ?? 'Workflows';}
public function navigationLabel(): string{ return 'Workflows'; // the navigation keeps the name of the page}
public function subtitle(): ?string{ return $this->selected() === null ? null : 'Workflows';}
public function breadcrumbs(): array{ $selected = $this->selected();
return $selected === null ? [$this->url() => 'Workflows'] : [$this->url() => 'Workflows', $this->url() . '?workflow=' . rawurlencode($selected->key()) => $selected->title()];}Check the query value against the entries the user may see; an unknown value shows the list. No own “back” links: the breadcrumb leads back.
Permissions of pages
Section titled “Permissions of pages”The permission of a page is e.g. admin.page.reports; it is listed in the
role editor. Users without it don’t see the navigation entry and get a
“no access” page (403, audit event access.denied). Return null from
permission() only for pages without sensitive data: every user of the
panel may open them.
The content is rendered on every request: load data in content(), not in
the constructor, and query through resources ($resource->query()) so
that tenant and scopeQuery() apply.
Interactive pages
Section titled “Interactive pages”Pages that need interactive parts – buttons that run code, forms, lists
that reload – implement Anvil\Page\HasComponents:
use Anvil\Endpoint\Component;use Anvil\Page\HasComponents;
final class SettingsPage extends Page implements HasComponents{ public function component(string $kind): ?Component { return $kind === 'settings' ? new SettingsComponent() : null; }}Components are addressed as <panel>:<page slug>:<kind> and go through the
component endpoint with signed tokens and
the permission of each action. The permission of the page applies to its
components as well: a token issued before the permission was revoked
opens nothing. Examples: Anvil\Access\ProfilePage, and
Components for writing components.
Dashboard
Section titled “Dashboard”Dashboard is the start page at /admin, see Dashboard.
A panel without a dashboard has no start page; keep it in pages().