Skip to content

Relation managers ​

A RelationManager manages an Eloquent relation of the record from the parent Resource's own panel, with its own table (with search and sorting), inline create/edit form, its own authorization and actions. It is declared inside relations():

php
public function relations(): array
{
    return [
        RelationManager::make('tags')
            ->form([
                Section::make()->schema([
                    Text::make('name')->rules('required', 'string', 'max:255'),
                ]),
            ])
            ->query(function (EloquentRelation $query, Post $post): void {
                $query->where('name', '!=', 'Excluded by query() hook');
            })
            ->canCreate(fn (Post $post): bool => $post->title !== 'Locked')
            ->canDelete(fn (Post $post): bool => $post->title !== 'Locked')
            ->columns([
                Column::make('name')->sortable()->searchable(),
                Column::make('is_primary')
                    ->as('boolean')
                    ->value(fn (Tag $tag): bool => (bool) $tag->pivot->is_primary),
            ])
            ->actions([
                AttachAction::make(),

                DetachAction::make()
                    ->authorize(fn (Model $parent): bool => $parent->tags()->count() > 1),

                Action::make('create_and_attach')
                    ->label('New tag')
                    ->icon('plus-circle')
                    ->standalone()
                    ->schema([
                        Text::make('name')->rules('required', 'string', 'max:255'),
                    ])
                    ->handle(function (array $data, Post $post): array {
                        $tag = Tag::firstOrCreate(['name' => $data['name']]);
                        $post->tags()->syncWithoutDetaching([$tag->id]);

                        return $tag->only(['id', 'name']);
                    }),
            ]),
    ];
}

RelationManager::make($relationship) takes the name of the relation method on the Eloquent model ($post->tags() in this case). columns() uses exactly the same Column API as a Resource, sortable()/searchable() included; the value()/color() callbacks receive the related model, not the parent (here, Tag, including its pivot).

Inline create/edit form ​

form() declares the schema of a form of the relation's own, reusing the same HasSchemaFields trait that Resource and Page already use, validation, moving uploaded files and RichEditor sanitization come for free, with nothing else to wire up:

php
RelationManager::make('tags')
    ->form([
        Section::make()->schema([
            Text::make('name')->rules('required', 'string', 'max:255'),
        ]),
    ])

With a form() declared and canCreate()/canEdit() authorizing it, the panel shows a "New {label}" button and each row can be opened to be edited inline, without going through any custom Action. It is a different concept from a standalone() action such as create_and_attach in the example above (which is an action of its own with its own schema()/handle()): both can live on the same relation.

Labels, creation and pivot data ​

MethodDescription
label(string $label)Title of the section. By default, Str::headline() of the relation name.
createLabel(string $label)Text of the create button. By default, "New" followed by the label.
createUsing(Closure $callback)Replaces how the related record is created. See below.
editsPivot(bool $editsPivot = true)The edit form saves the data to the pivot table of a belongsToMany relation, not to the related model.

createUsing() receives the validated data and the parent record, and must return the created model. It is the place for a creation that is not a plain create() on the relation:

php
RelationManager::make('comments')
    ->createUsing(fn (array $data, Post $post): Comment => app(AddComment::class)->handle($post, $data))

If it does not return a model, a LogicException is thrown. A RuntimeException is answered with a 422 and its message.

Filters ​

filters() accepts the same filters as a Resource, and they are shown in the relation table's bar:

php
RelationManager::make('tags')
    ->filters([
        TernaryFilter::make('is_primary')->label('Primary'),
    ])

Query and eager loading ​

php
->query(function (EloquentRelation $query, Post $post): void {
    $query->where('name', '!=', 'Excluded by query() hook');
})

The callback receives the Eloquent relation itself (an instance of HasMany, BelongsToMany...), not a bare query builder, so that where()/orderBy()/with() chain on it exactly as they would in a Resource's getEloquentQuery(). It must mutate the relation it receives, not try to build and return another one.

It is not optional in practice: an application that disables lazy loading outside production will blow up on the first column of a RelationManager that touches another relation, because the default $parent->{relationship}() query does not eager load anything on its own, this is where the with(...) would go.

Authorization ​

Four closures, evaluated against the parent record, which appear the other way round from a Resource's (which evaluates against the record itself, not its parent):

php
->canView(fn (Post $post): bool => ...)
->canCreate(fn (Post $post): bool => $post->title !== 'Locked')
->canEdit(fn (Post $post): bool => ...)
->canDelete(fn (Post $post): bool => $post->title !== 'Locked')

If you declare none, they all return true. In addition, the parent Resource must also allow the operation: viewing the relation requires the parent's canView(), and creating, editing or deleting there requires the parent's canEdit(). canCreate()/canEdit() control the "New" button and whether a row opens when clicked; the result of the four is sent per row as _authorizedActions, so the frontend never renders a button for an action it knows will be rejected, previously you had to click to discover a 403.

Actions inside a relation ​

actions() uses the same Action API, with two specific to this context:

AttachAction ​

php
AttachAction::make(); // icon 'plus', standalone(), schema amb un camp 'id'

It comes with a default handler:

php
$action->handle(fn (array $data, Model $parent): mixed => $parent->{$relationship}()->attach(
    $data['id'],
    Arr::except($data, ['id']),
));

Any additional key in data (beyond id) is passed as pivot attributes to attach(), useful for belongsToMany relations with pivot columns of their own.

DetachAction ​

php
DetachAction::make(); // icon 'x-mark', requiresConfirmation()

Default handler:

php
$action->handle(fn (array $data, Model $parent, Model $related): mixed => $parent->{$relationship}()->detach(
    $related->getKey(),
));

Note that the handler receives three arguments ($data, the parent and the related record), unlike a normal action on a Resource.

Custom actions inside a relation ​

Just like on a Resource, a normal Action::make(...) with its own schema()/handle() works the same inside a RelationManager, the create_and_attach example above creates a new Tag (or reuses an existing one by name) and attaches it to the Post, without going through AttachAction.

Looking up a relation from the Resource ​

php
$resource->relation('tags'); // ?RelationManager, for getRelationship() === 'tags'

Row actions with a form and messages ​

A normal Action inside a Relation manager is shown on each row, and supports the same things as on a Resource: schema() to ask for data, url() to be a link and a return message. The result of a handler that returns ['message' => '...'] is shown in the panel.

The Relation fields of a form or of an action of a Relation manager receive the parent record as the third argument of their query().