Skip to content

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() :

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

php
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éthodeDescription
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 :

php
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 :

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

Requête et eager loading ​

php
->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) :

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 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 ​

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

Elle vient avec un handler par défaut :

php
$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 ​

php
DetachAction::make(); // icône 'x-mark', requiresConfirmation()

Handler par défaut :

php
$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 ​

php
$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().