Relation managers
Un RelationManager gère une relation Eloquent de l'enregistrement depuis le même panneau que le Resource parent, avec son propre tableau (avec recherche et tri), un formulaire inline de création/édition, une autorisation propre et des actions. Il se déclare dans 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) prend le nom de la méthode de relation sur le modèle Eloquent ($post->tags() dans ce cas). columns() utilise exactement la même API de Column qu'un Resource, sortable()/searchable() compris ; les callbacks de value()/color() reçoivent le modèle lié, pas le parent (ici, Tag, avec son pivot).
Formulaire inline de création/édition
form() déclare le schema d'un formulaire propre à la relation, en réutilisant le même trait HasSchemaFields que Resource et Page utilisent déjà, la validation, le déplacement des fichiers envoyés et l'assainissement de RichEditor arrivent gratuitement, sans rien d'autre à brancher :
RelationManager::make('tags')
->form([
Section::make()->schema([
Text::make('name')->rules('required', 'string', 'max:255'),
]),
])Avec un form() déclaré et canCreate()/canEdit() qui l'autorisent, le panneau affiche un bouton « New {label} » et chaque ligne peut s'ouvrir pour être modifiée en ligne, sans passer par aucune Action sur mesure. C'est un concept différent d'une action standalone() comme create_and_attach dans l'exemple ci-dessus (qui est une action propre avec son propre schema()/handle()) : les deux peuvent cohabiter sur la même relation.
Libellés, création et données du pivot
| Méthode | Description |
|---|---|
label(string $label) | Titre de la section. Par défaut, Str::headline() du nom de la relation. |
createLabel(string $label) | Texte du bouton de création. Par défaut, « New » suivi du libellé. |
createUsing(Closure $callback) | Remplace la façon dont l'enregistrement lié est créé. Voir plus bas. |
editsPivot(bool $editsPivot = true) | Le formulaire d'édition enregistre les données dans la table pivot d'une relation belongsToMany, pas dans le modèle lié. |
createUsing() reçoit les données validées et l'enregistrement parent, et doit retourner le modèle créé. C'est l'endroit pour une création qui n'est pas un simple create() sur la relation :
RelationManager::make('comments')
->createUsing(fn (array $data, Post $post): Comment => app(AddComment::class)->handle($post, $data))S'il ne retourne pas un modèle, une LogicException est levée. Une RuntimeException reçoit un 422 et son message.
Filtres
filters() accepte les mêmes filtres qu'un Resource, et ils s'affichent dans la barre du tableau de la relation :
RelationManager::make('tags')
->filters([
TernaryFilter::make('is_primary')->label('Primary'),
])Requête et eager loading
->query(function (EloquentRelation $query, Post $post): void {
$query->where('name', '!=', 'Excluded by query() hook');
})Le callback reçoit la relation Eloquent elle-même (une instance de HasMany, BelongsToMany...), pas un query builder nu, pour que where()/orderBy()/with() s'y enchaînent exactement comme dans getEloquentQuery() d'un Resource. Il doit modifier la relation qu'il reçoit, pas essayer d'en construire et d'en retourner une autre.
Ce n'est pas facultatif en pratique : une application qui désactive le lazy loading hors production plantera à la première colonne d'un RelationManager qui touche une autre relation, car la requête $parent->{relationship}() par défaut ne précharge rien d'elle-même, c'est ici que se placerait le with(...).
Autorisation
Quatre closures, évaluées par rapport à l'enregistrement parent, qui apparaissent dans le sens inverse de celui d'un Resource (qui évalue par rapport à l'enregistrement lui-même, pas à son parent) :
->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 vous n'en déclarez aucune, elles retournent toutes true. De plus, le Resource parent doit aussi autoriser l'opération : voir la relation exige le canView() du parent, et y créer, modifier ou supprimer exige le canEdit() du parent. canCreate()/canEdit() contrôlent le bouton « New » et l'ouverture d'une ligne au clic ; le résultat des quatre est envoyé par ligne sous la forme _authorizedActions, de sorte que le frontend n'affiche jamais un bouton pour une action dont il sait qu'elle sera refusée, avant il fallait cliquer pour découvrir un 403.
Actions dans une relation
actions() utilise la même API d'Action, avec deux actions propres à ce contexte :
AttachAction
AttachAction::make(); // icône 'plus', standalone(), schema amb un camp 'id'Elle vient avec un handler par défaut :
$action->handle(fn (array $data, Model $parent): mixed => $parent->{$relationship}()->attach(
$data['id'],
Arr::except($data, ['id']),
));Toute clé supplémentaire dans data (au-delà de id) est passée comme attributs du pivot à attach(), utile pour les relations belongsToMany avec des colonnes pivot propres.
DetachAction
DetachAction::make(); // icône 'x-mark', requiresConfirmation()Handler par défaut :
$action->handle(fn (array $data, Model $parent, Model $related): mixed => $parent->{$relationship}()->detach(
$related->getKey(),
));Notez que le handler reçoit trois arguments ($data, le parent et l'enregistrement lié), contrairement à une action normale sur un Resource.
Actions sur mesure dans une relation
Comme sur un Resource, une Action::make(...) normale avec son propre schema()/handle() fonctionne de la même façon dans un RelationManager, l'exemple create_and_attach ci-dessus crée un nouveau Tag (ou en réutilise un existant par son nom) et l'attache au Post, sans passer par AttachAction.
Consulter une relation depuis le Resource
$resource->relation('tags'); // ?RelationManager, pour getRelationship() === 'tags'Actions par ligne avec formulaire et messages
Une Action normale dans un Relation manager s'affiche sur chaque ligne, et admet les mêmes choses que sur un Resource : schema() pour demander des données, url() pour être un lien et un message de retour. Le résultat d'un handler qui retourne ['message' => '...'] s'affiche dans le panneau.
Les champs Relation d'un formulaire ou d'une action d'un Relation manager reçoivent l'enregistrement parent comme troisième argument de leur query().