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():
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:
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étodo | Descripció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:
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:
RelationManager::make('tags')
->filters([
TernaryFilter::make('is_primary')->label('Primary'),
])Consulta y eager loading
->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):
->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
AttachAction::make(); // icono 'plus', standalone(), schema amb un camp 'id'Viene con un handler por defecto:
$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
DetachAction::make(); // icono 'x-mark', requiresConfirmation()Handler por defecto:
$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
$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().