Maîtrisez les relations belongs-to-many avec la méthode attach

Publié le 21 février 2023 par Mathieu De Gracia
Mis à jour le 13 avril 2024
Couverture de l'article Maîtrisez les relations belongs-to-many avec la méthode attach

Laravel dispose d'une relation belongsToMany permettant la définition d'une relation Many to Many(n-n) entre deux modèles.

L’exemple le plus courant et probablement le plus simple à apprehender est celui d’une gestion de rôles : “un utilisateur peut disposer d'un à plusieurs rôles et un rôle peut être attaché à un ou plusieurs utilisateurs”.

Afin de manipuler cette relation le framework propose une méthode attach facilitant l’utilisation du belongsToMany, voyons plus précisément ce qu'elle permet !

Prérequis

Avant de poursuivre la suite de ce tutoriel, nous allons créer les 3 tables suivantes :

1Schema::create('users', function (Blueprint $table) {
2 $table->id();
3 $table->timestamps();
4});
5 
6Schema::create('roles', function (Blueprint $table) {
7 $table->id();
8 $table->string('name');
9 $table->timestamps();
10});
11 
12Schema::create('role_user', function (Blueprint $table) {
13 $table->id();
14 $table->unsignedBigInteger('user_id');
15 $table->unsignedBigInteger('role_id');
16 $table->timestamps();
17});

La table role_user qu'on appellera pour la suite de ce tuto "pivot" servira de table intermédiaire permettant de lier les utilisateurs à des roles.

Ajoutons également les models User et Role ayant chacun une relation belongsToMany et leur factories respectives :

1class User extends Model
2{
3 use HasFactory;
4 
5 public function roles()
6 {
7 return $this->belongsToMany(Role::class);
8 }
9}
10 
11class Role extends Model
12{
13 use HasFactory;
14 
15 public function users()
16 {
17 return $this->belongsToMany(User::class);
18 }
19}

La commande make peut vous simplifier la création des modèles et de leurs factories associées. : php artisan make:model Role --factory

Pour le moment la table role_user ne possède pas de Model, nous reviendrons sur ce détail plus tard dans ce tutoriel.

Les bases de la méthode attach

Apres avoir créé des instances de User et de Role, utilisons la méthode attach pour assigner un role "admin" à notre utilisateur :

1$user = User::factory()->create();
2 
3$role = Role::factory()->create([
4 'name' => 'admin',
5]);
6 
7$user->roles()->attach($role);

Une ligne apparaît désormais dans notre table pivot role_user liant les deux models comme dans notre schema suivant :

Nous pouvons des lors facilement récupérer les roles d'un utilisateur depuis notre relation roles() du model User :

1$user = User::first();
2 
3$roles = $user->roles()->get();
4 
5dd($roles->toArray());
6 
7# array:4 [
8# "id" => 1
9# "name" => "admin",
10# "created_at" => "2023-02-20T18:57:47.000000Z"
11# "updated_at" => "2023-02-20T18:57:47.000000Z"
12# "pivot" => array:4 [
13# "user_id" => 1
14# "role_id" => 1
15# ]
16# ]

On retrouve ainsi dans le dump les différents roles de notre utilisateur, dans le cas présent le role Admin !

Pour la suite de ce tutoriel, nous utiliserons la méthode with pour accéder plus rapidement à la relation roles !

Attacher plusieurs éléments

Nous venons d’utiliser notre premiere relation belongsToMany en assignant un role à un utilisateur, afin de faciliter une création en masse, il est envisageable d’envoyer une liste de role à associer à la méthode attach :

1$user = User::factory()->create();
2 
3$roles = Role::factory()->count(3)->create();
4 
5$user->roles()->attach($roles);

La méthode peut recevoir une instance de model, une collection, un array …

Désormais, nos 3 models roles bien sont liés au model User, nous pouvons vérifier les ids des roles associés avec un pluck !

1$user = User::with('roles')->first();
2 
3$user->roles->pluck('id'); // 1, 2, 3

Attention, gardez en tête que la méthode attach ne vous protège pas des doublons !

Dans la situation suivante, la même instance de role sera associé 3 fois à l’utilisateur.

1$user = User::factory()->create();
2 
3$role = Role::factory()->create();
4 
5$user->roles()->attach($role);
6$user->roles()->attach($role);
7$user->roles()->attach($role);
8 
9$user = User::with('roles')->first();
10 
11$user->roles->pluck('id'); // 1, 1, 1

Attacher avec des attributs

Le pivot étant une table intermédiaire entre users et roles elle peut également contenir des informations propre à la relation !

Imaginons par exemple que nous souhaitons ajouter une notion d'activation à la relation, pour ce faire, ajoutons un nouveau champ "active" à notre migration role_user :

1Schema::create('role_user', function (Blueprint $table) {
2 $table->id();
3 $table->unsignedBigInteger('user_id');
4 $table->unsignedBigInteger('role_id');
5 $table->integer('active')->default(0);
6 $table->timestamps();
7});

La commande php artisan migrate:fresh vous aidera à remettre la bdd à zero !

Bien qu’une table pivot soit en partie gérée par le framework, vous êtes totalement libre d’y ajouter autant de colonne que vous le souhaitez.

Lors de l’utilisation de la méthode attach, un second argument attributes est disponible afin de transmettre des valeurs additionnelles à sauvegarder dans le pivot, dans notre cas nous souhaitons ajouter une valeur active :

1$user = User::factory()->create();
2 
3$role = Role::factory()->create();
4 
5$user->roles()->attach($role, attributes: [
6 'active' => 0,
7]);

Ensuite, vous devez renseigner l'existence de cette valeur directement dans la relation du model User grace à la méthode withPivot :

1class User extends Model
2{
3 public function roles()
4 {
5 return $this->belongsToMany(Role::class)->withPivot('active');
6 }
7}

Il est possible d’envoyer un array ou plusieurs arguments successifs à withPivot.

Désormais, quand vous récupérerez les roles d'un utilisateur, la valeur du champ active sera directement accessible.

1$user = User::with('roles')->first();
2 
3$role = $user->roles->first();
4 
5$role->pivot->active; // 0

Récupérer les timestamps d’un pivot

Notre table pivot possède les timestamps created_at et updated_at correspondant respectivement à la date de création du pivot et à sa dernière modification.

Par défaut, ces valeurs ne sont pas présentes lors de la récupération des pivots depuis une relation belongsToMany pour les obtenir il est nécessaire de modifier la relation roles et d'y ajouter la méthode withTimestamps.

1class User extends Model
2{
3 public function roles()
4 {
5 return $this->belongsToMany(Role::class)->withTimestamps();
6 }
7}

Désormais, le pivot possède ses timestamps !

1$user = User::with('roles')->first();
2 
3$role = $user->roles->first();
4 
5dd($role->pivot->toArray());
6 
7# array:4 [
8# "user_id" => 11
9# "role_id" => 22
10# "created_at" => "2023-02-20T18:44:33.000000Z"
11# "updated_at" => "2023-02-20T18:44:33.000000Z"
12# ]

Ajouter un model au pivot

Jusqu'à présent, le table role_user n'était pas matérialisée par un model dans notre application, si vous le souhaitez, il est possible d'en ajouter un afin de profiter de quelques fonctionnalités supplémentaires.

1namespace App\Models;
2 
3use Illuminate\Database\Eloquent\Relations\Pivot;
4 
5class RoleUser extends Pivot
6{
7 //
8}

La class Pivot hérite de la class Model, cela signifie que RoleUser bénéficie de toutes les fonctionnalités de Eloquent ! Cette class peut donc profiter des casts, des relations, des events ...

Dans notre exemple, essayons de cast le champ active en boolean :

1class RoleUser extends Pivot
2{
3 protected function casts(): array
4 {
5 return [
6 'active' => 'boolean',
7 ];
8 }
9}

Pour finir, vous n'avez plus qu'à renseigner l'utilisation de cette class pivot dans la relation et le tour est joué !

1class User extends Model
2{
3 public function roles()
4 {
5 return $this->belongsToMany(Role::class)
6 ->withPivot('active')
7 ->using(RoleUser::class);
8 }
9}

Il reste nécessaire de renseigner le withPivot pour charger la propriété "active"

Nous récupérons dorénavant les roles depuis la class pivot RoleUser et profitons du cast en boolean !

1$user = User::factory()->create();
2 
3$role = Role::factory()->create();
4 
5$user->roles()->attach($role, attributes: [
6 'active' => 1,
7]);
8 
9$user->refresh();
10 
11$role = $user->roles->first();
12 
13$role->pivot->active; // true

Toggle

Le toggle est une méthode permettant de basculer les relations d'un belongsToMany.

Si la méthode toggle reçoit une instance de Role qui n'est pas présente dans la table role_user, elle sera alors ajoutée.

Dans le cas contraire, si la relation est d'hors et deja présente, cette dernière sera supprimée.

1$user = User::factory()->create();
2$role = Role::factory()->create();
3 
4$user->roles()->toggle($role);
5 
6$user->roles->pluck('id'); // 1
7 
8$user->roles()->toggle($role);
9 
10$user->refresh();
11 
12$user->roles->pluck('id'); // null

L'utilisation du refresh est nécessaire pour rafraichir les relations de $user

Mathieu De Gracia avatar
Mathieu De Gracia
Des fois, mon chat code à ma place 🐱

A lire

Autres articles de la même catégorie