Skip to content

Relation managers ​

Un RelationManager gestiona una relación Eloquent del registro desde el mismo panel del Resource padre, con su propia tabla (con búsqueda y ordenación), formulario inline de crear/editar, autorización propia y acciones. Se declara dentro de 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) toma el nombre del método de relación del modelo Eloquent ($post->tags() en este caso). columns() usa exactamente la misma API de Column que un Resource, sortable()/searchable() incluidos; los callbacks de value()/color() reciben el modelo relacionado, no el padre (aquí, Tag, incluyendo su pivot).

Formulario inline de crear/editar ​

form() declara el schema de un formulario propio de la relación, reutilizando el mismo trait HasSchemaFields que ya usan Resource y Page, la validación, el movimiento de archivos subidos y la sanitización de RichEditor llegan gratis, sin nada más que conectar:

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

Con un form() declarado y canCreate()/canEdit() autorizándolo, el panel muestra un botón «New {label}» y cada fila se puede abrir para editarse inline, sin pasar por ninguna Action a medida. Es un concepto distinto de una acción standalone() como create_and_attach del ejemplo de arriba (que es una acción propia con su propio schema()/handle()): ambos pueden convivir en la misma relación.

Etiquetas, creación y datos del pivote ​

MétodoDescripción
label(string $label)Título de la sección. Por defecto, Str::headline() del nombre de la relación.
createLabel(string $label)Texto del botón de crear. Por defecto, «New» seguido de la etiqueta.
createUsing(Closure $callback)Sustituye cómo se crea el registro relacionado. Véase más abajo.
editsPivot(bool $editsPivot = true)El formulario de edición guarda los datos en la tabla pivote de una relación belongsToMany, no en el modelo relacionado.

createUsing() recibe los datos validados y el registro padre, y debe devolver el modelo creado. Es el lugar para una creación que no es un simple create() sobre la relación:

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

Si no devuelve un modelo, se lanza una LogicException. Una RuntimeException se responde con un 422 y su mensaje.

Filtros ​

filters() acepta los mismos filtros que un Resource, y se muestran en la barra de la tabla de la relación:

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

Consulta y eager loading ​

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

El callback recibe la relación Eloquent en sí (una instancia de HasMany, BelongsToMany...), no un query builder pelado, para que where()/orderBy()/with() se encadenen en ella exactamente igual que lo harían en getEloquentQuery() de un Resource. Debe mutar la relación que recibe, no intentar construir y devolver otra.

No es opcional en la práctica: una aplicación que desactiva el lazy loading fuera de producción reventará en la primera columna de un RelationManager que toque otra relación, porque la query $parent->{relationship}() por defecto no precarga nada por sí misma, es aquí donde estaría el with(...).

Autorización ​

Cuatro closures, evaluadas contra el registro padre, que aparecen en sentido inverso al de un Resource (que evalúa contra el propio registro, no su padre):

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')

Si no declaras ninguna, todas devuelven true. Además, el Resource padre también debe permitir la operación: ver la relación exige canView() del padre, y crear, editar o borrar en ella exige canEdit() del padre. canCreate()/canEdit() controlan el botón «New» y si una fila se abre al hacer clic; el resultado de las cuatro se envía por fila como _authorizedActions, así que el frontend nunca renderiza un botón de una acción que sabe que será rechazada, antes había que hacer clic para descubrir un 403.

Acciones dentro de una relación ​

actions() usa la misma API de Action, con dos específicas de este contexto:

AttachAction ​

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

Viene con un handler por defecto:

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

Cualquier clave adicional en data (más allá de id) se pasa como atributos del pivote a attach(), útil para relaciones belongsToMany con columnas pivote propias.

DetachAction ​

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

Handler por defecto:

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

Ten en cuenta que el handler recibe tres argumentos ($data, el padre y el registro relacionado), a diferencia de una acción normal sobre un Resource.

Acciones a medida dentro de una relación ​

Igual que en un Resource, una Action::make(...) normal con su propio schema()/handle() funciona igual dentro de un RelationManager, el ejemplo de create_and_attach de arriba crea una Tag nueva (o reutiliza una existente por nombre) y la adjunta al Post, sin pasar por AttachAction.

Consultar una relación desde el Resource ​

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

Acciones por fila con formulario y mensajes ​

Una Action normal dentro de un Relation manager se muestra en cada fila, y admite las mismas cosas que en un Resource: schema() para pedir datos, url() para ser un enlace y un mensaje de retorno. El resultado de un handler que devuelve ['message' => '...'] se muestra en el panel.

Los campos Relation de un formulario o de una acción de un Relation manager reciben el registro padre como tercer argumento de su query().