Related records
The detail page shows related records, e.g. the items of an order, in a list of the related resource: same columns, filters and actions, restricted to the records of the parent.
use Anvil\Resource\Relation;
public function relations(): array{ return [ Relation::make('items')->resource(OrderItemResource::class)->foreignKey('order_id')->label('Items'), ];}Relation options
Section titled “Relation options”| Method | Purpose |
|---|---|
Relation::make('items') |
name ([a-z][a-z0-9_]*), unique in the resource |
resource(Resource::class) |
the related resource (required); must be registered in the same panel |
foreignKey('order_id') |
column of the related table that references the parent (one-to-many) |
pivot(Model::class, 'parent_key', 'related_key') |
link through a pivot model instead (many-to-many) |
label('…') |
heading; default: plural label of the related resource |
editable() |
editable columns of the related resource can be changed in this list, see below |
attachable(…) |
Attach links existing records, see below |
detachable(…) |
Remove unlinks records |
headerActions([...]) |
own actions above the related records |
recordActions([...]) |
own actions in the rows |
Rules:
- The permissions of the related resource apply: users without
order-items.viewdo not see the section. - The parent record must be visible to the user, otherwise 404.
- The list uses the query of the related resource (tenant,
scopeQuery(), policies) plus the condition of the relation. - Related lists offer the column menu, but neither remember the layout nor offer saved views or widgets.
As tabs
Section titled “As tabs”Show the sections below the form as tabs instead of one below the other:
protected bool $relationTabs = true;- One tab per relation the user may see, in the order of
relations(); each tab shows the label and the number of related records. Actions and New are inside the tab. - Then one tab per section that has content: workflows, owner and deputies,
checklist, own
detailComponents(), comments, attachments and history. A component names its tab withlabel()of the interfaceAnvil\Resource\LabelledComponent; without it the tab is named after its kind ('contracts'→ Contracts). - The open tab is remembered per resource in the browser tab
(
sessionStorage): after creating a related record or running an action the user is back on the same tab. - The form, components, attachments and history stay outside the tabs.
Editing in the list
Section titled “Editing in the list”Related lists show values. With editable() the
editable columns of the related resource
become controls, e.g. what is ordered in the lines of an order:
Relation::make('lines')->resource(OrderLineResource::class)->foreignKey('order_id')->editable(),- Changing a value needs the update permission (with policy) of the related record and of the parent, so a locked parent keeps its related records.
- Every request checks again that the record belongs to the parent.
- The foreign key to the parent is never a control.
Create, attach and detach
Section titled “Create, attach and detach”| Button | Enabled by | Effect |
|---|---|---|
| New | canCreate() of the related resource |
creates a related record linked to the parent |
| Attach | attachable() |
links an existing record (foreign key set, or pivot record created) |
| Remove (per row) | detachable() |
unlinks the record (foreign key cleared, or pivot record deleted) |
// One-to-many: deals have a nullable contact_id.Relation::make('deals')->resource(DealResource::class)->foreignKey('contact_id') ->attachable(titleAttribute: 'title') ->detachable(),
// Many-to-many through a pivot model (an Anvil model: tenant scoped, audited).Relation::make('labels')->resource(LabelResource::class) ->pivot(ProjectLabelModel::class, 'project_id', 'label_id') ->attachable() ->detachable(),php spark make:anvil-pivot ProjectLabel --parent projects --related labels
generates the pivot table and model, see
Commands.
New opens the create page of the related resource with the parent. The foreign key (or the pivot record) is set from the encrypted token and verified again on saving; a value submitted for the foreign key is ignored. After saving the user is back on the parent.
Edit in a related list opens the edit page with the parent in the breadcrumbs. The request checks that the record belongs to the parent, and after saving or cancelling the user is back on the parent.
Attach
Section titled “Attach”attachable(titleAttribute: null, scope: null, permission: null):
- offers, in a searchable select, the records the user may see in the related resource that are not linked yet,
titleAttributeis shown in the select (default: the record title attribute of the related resource),scope: fn (Model $model) => …restricts the records further,permissionreplaces the default<parent slug>.update.
Remove
Section titled “Remove”detachable(using: null, visible: null, permission: null, label: null, confirm: null):
- deletes only the link, never the related record; with
foreignKey()the column must be nullable, - asks for confirmation,
using: fn ($related, $data, $parent) => …replaces the default, e.g. to end a membership with a date;visibledecides per row.
Permissions
Section titled “Permissions”New, Attach and Remove require <parent slug>.update (with the
policy of the parent) unless a permission is passed; New also needs
canCreate() of the related resource. With a foreign key, Attach and
Remove change the related record: they also need its update permission
(with policy), so records of other parents cannot be moved without it.
Every request checks again that the record belongs to the parent.
Deleting a related record is still the Delete button of the related
resource.
Relation actions
Section titled “Relation actions”Domain specific membership, e.g. an assignment with a validity period, uses actions on the relation:
headerActions()are shown above the related records, receive the parent record and may have a form.recordActions()are shown in the rows and receive the related record, the form data and the parent record; they cannot have a form (LogicException), useconfirm()for a question.
Relation::make('assignments')->resource(RoleAssignmentResource::class)->foreignKey('business_role_id') ->headerActions([ Action::make('assignPerson')->label('Assign person')->icon('person-plus') ->form(fn (Form $form) => $form->schema([ BelongsTo::make('person_id')->relationship(PersonModel::class, 'last_name')->searchable()->required(), DateInput::make('valid_from')->required(), ])) ->handle(function ($role, array $data): string { service('assignments')->assign($role, $data);
return 'The person has been assigned.'; }), ]) ->detachable( using: fn ($assignment) => service('assignments')->end($assignment), visible: fn ($assignment) => $assignment['ended_at'] === null, label: 'End', confirm: 'End this assignment today?', );Relation actions require <parent slug>.update unless they name their own
permission(). All options of actions: Actions.