#3 découverte FilamentPHP : les relations

Publié le 11 mars 2024 par Mathieu De Gracia
Couverture de l'article #3 découverte FilamentPHP : les relations

Ce tutoriel est le troisième chapitre de notre série concernant la découverte de FilamentPHP :

Dans cet article, nous allons ajouter une notion de rôle à nos utilisateurs : un utilisateur pourra posséder un ou plusieurs rôles à travers une relation.

Voyons aujourd'hui ce que propose Filament afin de manipuler des relations entre nos différentes ressources !

Prérequis

Qui dit relation dit nouvelle table, commençons par créer un nouveau modèle Role ainsi que sa migration associée à l'aide de la commande suivante :

1php artisan make:model Role --migration

La relation rôles sera de type belongsToMany, nous vous conseillons notre tutoriel si vous voulez en savoir plus sur le fonctionnement et les possibilités offertes par ce type de relation.

Un rôle est donc lié à des utilisateurs :

1 
2namespace App\Models;
3 
4use App\Models\User;
5use Illuminate\Database\Eloquent\Model;
6use Illuminate\Database\Eloquent\Factories\HasFactory;
7 
8class Role extends Model
9{
10 use HasFactory;
11 
12 public function users()
13 {
14 return $this->belongsToMany(User::class);
15 }
16}

Modifions également notre modèle User pour y ajouter la relation inverse, un utilisateur possède des rôles :

1<?php
2 
3namespace App\Models;
4 
5use App\Models\Role;
6use Laravel\Sanctum\HasApiTokens;
7use Illuminate\Notifications\Notifiable;
8use Illuminate\Database\Eloquent\Factories\HasFactory;
9use Illuminate\Foundation\Auth\User as Authenticatable;
10 
11class User extends Authenticatable
12{
13 use HasApiTokens, HasFactory, Notifiable;
14 
15 // [...]
16 
17 public function roles()
18 {
19 return $this->belongsToMany(Role::class);
20 }
21}

Il sera ensuite indispensable de modifier la migration, précédemment créée à l'aide de la commande artisan, afin d'y ajouter une table intermédiaire role_user faisant le lien entre des rôles et des utilisateurs, ainsi qu'une propriété name sur la table roles :

1use Illuminate\Database\Migrations\Migration;
2use Illuminate\Database\Schema\Blueprint;
3use Illuminate\Support\Facades\Schema;
4 
5return new class extends Migration
6{
7 /**
8 * Run the migrations.
9 */
10 public function up(): void
11 {
12 Schema::create('roles', function (Blueprint $table) {
13 $table->id();
14 $table->string('name');
15 $table->timestamps();
16 });
17 
18 Schema::create('role_user', function (Blueprint $table) {
19 $table->id();
20 $table->unsignedBigInteger('user_id');
21 $table->unsignedBigInteger('role_id');
22 $table->timestamps();
23 });
24 }
25 
26 /**
27 * Reverse the migrations.
28 */
29 public function down(): void
30 {
31 Schema::dropIfExists('roles');
32 Schema::dropIfExists('role_user');
33 }
34};

Pour finir, utilisons le DatabaseSeeder afin d'insérer des rôles en base de données, nous n'utiliserons pas de factory en raison de la simplicité de notre modèle :

1 
2namespace Database\Seeders;
3 
4use Illuminate\Database\Seeder;
5use Illuminate\Support\Facades\DB;
6 
7class DatabaseSeeder extends Seeder
8{
9 /**
10 * Seed the application's database.
11 */
12 public function run(): void
13 {
14 DB::table('roles')->insert([
15 ['name' => 'admin'],
16 ['name' => 'member'],
17 ['name' => 'contributor'],
18 ]);
19 }
20}

Exécutez maintenant les commandes artisan migrate et db:seed et notre application sera prête à manipuler cette nouvelle relation !

Créer la ressource Rôle

Le rôle étant une ressource comme les autres, nous devons la créer dans notre projet à l'aide de la commande suivante :

1php artisan make:filament-resource Role

Nous n'allons pas détailler ici la création des différentes page constituant la ressource "Role", cette dernière sera, on ne peut plus classique, possédant un unique champ "name".

Vous trouverez dans les deux premiers chapitres de cette série de tutoriels toutes les informations nécessaires afin d'ajouter cette nouvelle ressource à votre application.

Le code de la ressource Rôle est disponible à cette URL.

Afficher le rôle d'un utilisateur

Afficher la valeur d'une relation se fera depuis le composant TextColumn à l'aide d'une simple notation à point :

1public static function table(Table $table): Table
2{
3 return $table
4 ->columns([
5 TextColumn::make('name')
6 ->searchable(),
7 TextColumn::make('roles.name'),
8 TextColumn::make('email'),
9 // [...]
10 ])
11 // [...]
12}

Attardons-nous sur la notation roles.name : le mot roles fait référence à la relation belongsToMany présente dans le modèle User, tandis que name correspond à la propriété du modèle Role que nous souhaitons afficher.

Qu'importe le type de relation, vous n'aurez aucune préoccupation supplémentaire à avoir, notamment concernant des contraintes de edge loading, Filament se chargera de tout à votre place !

Il sera également possible d'utiliser des fonctions d'agrégation, telles que le count, pour déterminer le nombre de rôles de chaque utilisateur, toujours à l'aide du composant TextColumn :

1public static function table(Table $table): Table
2{
3 return $table
4 ->columns([
5 TextColumn::make('name')
6 ->searchable(),
7 TextColumn::make('roles.name'),
8 TextColumn::make('roles_count')
9 ->counts('roles')
10 ->sortable()
11 ->label('NB roles'),
12 TextColumn::make('email'),
13 // [...]
14 ])
15 // [...]
16}

Attention, veuillez ici à bien nommer votre TextColumn, sa valeur devra impérativement correspondre au nom de la relation suffixé par _count, dans le cas contraire, Filament lèvera une erreur.

Pour finir, ajoutons un simple filtre permettant de rapidement trier nos utilisateurs à l'aide du composant SelectFilter :

1public static function table(Table $table): Table
2{
3 return $table
4 ->columns([
5 // [...]
6 ])
7 ->defaultSort('created_at', 'desc')
8 ->filters([
9 SelectFilter::make('roles')
10 ->relationship('roles', 'name'),
11 ])
12 ->actions([
13 // [...]
14 ])
15 ->bulkActions([
16 // [...]
17 ]);
18}

Quelques options supplémentaires sur votre SelectFilter le rendrons plus réactif en ajoutant, par exemple, la possibilité de rechercher dans les rôles présents ou bien encore en chargeant en amont les différentes relations en mémoire à l'affichage de la page.

1SelectFilter::make('roles')
2 ->relationship('roles', 'name')
3 ->badge()
4 ->searchable()
5 ->preload(),

Notre listing est désormais opérationel, nous affichons les rôles de chaque utilisateur et avons la possibilité de les filtrer.

Il est désormais temps d'ajouter cette notion de rôle dans les formulaires d'édition et de création d'un utilisateur !

Modifier le rôle d'un utilisateur

L'ajout de la gestion des rôles dans les pages d'édition et de création d'un utilisateur sera tout aussi simple.

Commençons par le formulaire de création, rappelez-vous, nous avions ajouté un wizard dans notre précédent chapitre, ce dernier se trouve dans le fichier CreateUser.

Rappel : un Wizard est un formulaire en multi étapes

1public function getSteps(): array
2{
3 return [
4 Step::make('Information')
5 ->schema([
6 // [...]
7 ]),
8 Step::make('Password')
9 ->schema([
10 // [...]
11 ]),
12 Step::make('Validation')
13 ->schema([
14 Select::make('roles')
15 ->relationship('roles', 'name'),
16 Toggle::make('auto_verified')
17 ->label('Validate user email')
18 ->default(false),
19 ]),
20 ];
21}

Un composant Select sera amplement suffisant afin de proposer les différents rôles disponibles depuis le wizard de création des utilisateurs.

Concernant la méthode relationship, le premier argument sera le nom de la relation (roles) tandis que le second sera le nom de la propriété du modèle Role à afficher (name).

Ajoutons également la possibilité de sélectionner plusieurs rôles à l'aide de la méthode multiple ainsi qu'un pré-chargement des rôles similaire à celui présent sur notre page de listing.

1Select::make('roles')
2 ->relationship('roles', 'name')
3 ->multiple()
4 ->preload(),

À l'instar des autres champs, celui-ci peut être rendu obligatoire !

Le formulaire de création est désormais fonctionnel et vous permettra de sélectionner un ou plusieurs rôle à votre utilisateur !

Concernant l'édition, il suffira d'insérer quelques lignes similaires dans la méthode form de notre UserResource et le tour sera joué :

1public static function form(Form $form): Form
2{
3 return $form
4 ->schema([
5 Section::make()
6 ->schema([
7 TextInput::make('name')
8 ->required()
9 ->maxLength(10),
10 TextInput::make('email')
11 ->disabled(),
12 Select::make('roles')
13 ->relationship('roles', 'name')
14 ->multiple()
15 ->preload(),
16 DatePicker::make('email_verified_at')
17 ->maxDate(now()),
18 ])
19 ]);
20}

Relation manager

Nos modèles rôles sont liés à des utilisateurs grâce à la relation belongsToMany que nous avons précédemment créée : un rôle possède des utilisateurs et inversement.

Il se peut que nous soyons un jour amenés à vouloir afficher tous les utilisateurs d'un rôle en particulier, pour répondre à ce besoin, Filament met à disposition les relations manager.

Un relation manager nous permettra d'afficher tous les relations associés à un modèle spécifique depuis la page d'édition de ce dernier.

Pour commencer, créons un relation manager à l'aide de la commande suivante :

1php artisan make:filament-relation-manager UserResource users name

L'exécution de la commande précédente devrait vous amener à créer une nouvelle classe UsersRelationManager, cette dernière devra être référencée dans la méthode getRelations de notre RoleResource pour être prise en compte :

1public static function getRelations(): array
2{
3 return [
4 UsersRelationManager::class,
5 ];
6}

Notre relation manager est désormais pratiquement prêt, cependant, il lui manque quelques configurations afin d'être pleinement opérationnel.

Un RelationManager fraîchement créé possède, par défaut, deux méthodes : form et table :

1class UsersRelationManager extends RelationManager
2{
3 protected static string $relationship = 'users';
4 
5 public function form(Form $form): Form
6 {
7 return $form
8 ->schema([
9 //
10 ]);
11 }
12 
13 public function table(Table $table): Table
14 {
15 return $table
16 ->recordTitleAttribute('name')
17 ->columns([
18 //
19 ])
20 ->filters([
21 //
22 ])
23 ->headerActions([
24 //
25 ])
26 ->actions([
27 Tables\Actions\EditAction::make(),
28 Tables\Actions\DeleteAction::make(),
29 ])
30 ->bulkActions([
31 Tables\Actions\BulkActionGroup::make([
32 Tables\Actions\DeleteBulkAction::make(),
33 ]),
34 ]);
35 }
36}

Dans sa structure, un RelationManager est très proche d'une resource

La méthode table configure l'affichage de notre utilisateur à travers le relation manager, dans notre cas, nous souhaitons uniquement afficher son nom :

1public function table(Table $table): Table
2{
3 return $table
4 ->recordTitleAttribute('name')
5 ->columns([
6 Tables\Columns\TextColumn::make('name'),
7 ])
8 // [...]
9}

La méthode form, quant à elle, permet d'ajouter un formulaire d'édition d'un utilisateur. Ce formulaire sera en doublon de celui précédemment créé dans notre UserResource ... cela signifie que nous pourrions modifier un utilisateur directement depuis l'affichage d'un rôle !

Ce comportement peut s'avérer perturbant et nous ne l'utiliserons pas dans ce tutoriel.

Notre relation manager est désormais complet et vous verrez désormais apparaître tous les utilisateurs associés à un rôle lorsque vous vous rendrez sur la page d'édition du rôle :

Dans ce troisième tutoriel, nous avons ajouté des relations entre nos modèles et mis en place notre première relation manager, dans le prochain chapitre, nous aborderons des tips et des conseils sur l'utilisation de FilamentPHP !

Source : https://github.com/laravel-fr/support-filamentphp
Mathieu De Gracia avatar
Mathieu De Gracia
Des fois, mon chat code à ma place 🐱

A lire

Autres articles de la même catégorie