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 |
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”With several relations, show them as tabs instead of sections 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. - 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.
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.
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.