Relations complexes avec Eloquent : hasMany, belongsToMany et morph
Introduction aux Relations Eloquent
Dans le monde du développement backend, la modélisation des données est cruciale. Une base de données relationnelle est par essence construite autour des liens entre les différentes entités (tables). Laravel, via son ORM Eloquent, simplifie grandement l'interaction avec ces bases de données en nous permettant de définir ces relations directement dans nos modèles PHP, rendant le code plus intuitif et maintenable.
Cet approfondissement est dédié à la maîtrise des relations plus "complexes" qui vont au-delà du simple "un-à-un" (hasOne, belongsTo). Nous explorerons en détail les relations "un-à-plusieurs" (hasMany), "plusieurs-à-plusieurs" (belongsToMany), et les puissantes relations polymorphiques (morphTo, morphMany, morphToMany). Comprendre et appliquer ces concepts est fondamental pour construire des applications Laravel robustes et bien structurées.
I. La Relation Un-à-Plusieurs : hasMany
La relation un-à-plusieurs est l'une des plus courantes en base de données. Elle décrit une situation où une entité parent peut avoir plusieurs entités enfants, mais chaque entité enfant n'appartient qu'à un seul parent.
Exemples concrets :
- Un
Utilisateur(parent) peut avoir plusieursArticles(enfants). - Une
Catégorie(parent) peut contenir plusieursProduits(enfants). - Un
Album(parent) peut contenir plusieursPhotos(enfants).
1.1. Modélisation de la Base de Données
Pour établir une relation hasMany, la table "enfant" doit contenir une clé étrangère qui référence la clé primaire de la table "parent".
Prenons l'exemple d'un Utilisateur et de ses Articles :
-
Table
users:id(PRIMARY KEY)nameemail- ...
-
Table
posts:id(PRIMARY KEY)user_id(FOREIGN KEY versusers.id)titlecontent- ...
Laravel s'attend par défaut à ce que la clé étrangère sur la table posts soit nommée user_id, et qu'elle pointe vers la colonne id de la table users. Si vos conventions de nommage sont différentes, vous devrez les spécifier dans la définition de la relation.
1.2. Définition des Relations Eloquent
Dans les modèles Eloquent, la relation hasMany est définie sur le modèle "parent", tandis que la relation inverse (belongsTo) est définie sur le modèle "enfant".
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
use HasFactory;
/**
* Obtenir tous les articles rédigés par l'utilisateur.
*/
public function posts()
{
// Un utilisateur a plusieurs articles
return $this->hasMany(Post::class);
// Syntaxe complète si les conventions de nommage sont différentes:
// return $this->hasMany(Post::class, 'foreign_key', 'local_key');
// 'foreign_key' est la clé étrangère sur la table 'posts' (ex: 'auteur_id')
// 'local_key' est la clé locale sur la table 'users' (ex: 'uuid' au lieu de 'id')
}
}
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
/**
* Obtenir l'utilisateur qui a rédigé cet article.
*/
public function user()
{
// Un article appartient à un utilisateur
return $this->belongsTo(User::class);
// Syntaxe complète si les conventions de nommage sont différentes:
// return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
// 'foreign_key' est la clé étrangère sur la table 'posts' (ex: 'auteur_id')
// 'owner_key' est la clé primaire sur la table 'users' (ex: 'uuid' au lieu de 'id')
}
}
1.3. Utilisation des Relations hasMany
Une fois définies, les relations peuvent être utilisées comme des propriétés dynamiques ou appelées comme des méthodes.
use App\Models\User;
use App\Models\Post;
// Créer un utilisateur et quelques articles
$user = User::create(['name' => 'Alice', 'email' => 'alice@example.com', 'password' => 'secret']);
$user->posts()->createMany([
['title' => 'Mon premier article', 'content' => 'Lorem ipsum...'],
['title' => 'Mon deuxième article', 'content' => 'Dolor sit amet...'],
]);
// Récupérer tous les articles d'un utilisateur
$user = User::find(1);
foreach ($user->posts as $post) {
echo "Article: " . $post->title . " par " . $user->name . "\n";
}
// Ajouter un nouvel article à un utilisateur existant
$user = User::find(1);
$user->posts()->create([
'title' => 'Un nouvel article d\'Alice',
'content' => 'Ceci est un contenu...'
]);
// Accéder au parent depuis l'enfant
$post = Post::find(1);
echo "Cet article a été écrit par : " . $post->user->name . "\n";
// Eager loading pour éviter le problème N+1
// Récupère l'utilisateur et tous ses articles en une seule requête pour les articles
$users = User::with('posts')->get();
foreach ($users as $user) {
echo "Utilisateur: " . $user->name . "\n";
foreach ($user->posts as $post) {
echo " - Article: " . $post->title . "\n";
}
}
II. La Relation Plusieurs-à-Plusieurs : belongsToMany
La relation plusieurs-à-plusieurs est nécessaire lorsque les deux entités peuvent être liées à plusieurs instances de l'autre entité.
Exemples concrets :
- Un
Utilisateurpeut avoir plusieursRôles, et unRôlepeut être attribué à plusieursUtilisateurs. - Un
Articlepeut avoir plusieursTags, et unTagpeut être associé à plusieursArticles. - Un
Étudiantpeut suivre plusieursCours, et unCourspeut avoir plusieursÉtudiants.
2.1. Modélisation de la Base de Données
Les relations plusieurs-à-plusieurs nécessitent l'utilisation d'une table intermédiaire (ou table pivot). Cette table contient généralement deux clés étrangères, chacune référençant la clé primaire de l'une des deux tables principales.
Prenons l'exemple d'un Utilisateur et de ses Rôles :
-
Table
users:id(PRIMARY KEY)name- ...
-
Table
roles:id(PRIMARY KEY)name- ...
-
Table pivot
role_user(conventionnellement nommée par ordre alphabétique des modèles singuliers) :user_id(FOREIGN KEY versusers.id)role_id(FOREIGN KEY versroles.id)- (Optionnel) Autres colonnes pour des informations supplémentaires sur la relation, ex:
created_at
2.2. Définition des Relations Eloquent
La relation belongsToMany est définie sur les deux modèles participants à la relation.
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
use HasFactory;
/**
* Obtenir les rôles de l'utilisateur.
*/
public function roles()
{
// Un utilisateur peut avoir plusieurs rôles
return $this->belongsToMany(Role::class);
// Syntaxe complète si les conventions de nommage sont différentes:
// return $this->belongsToMany(Role::class, 'nom_table_pivot', 'clé_étrangère_utilisateur', 'clé_étrangère_rôle');
// 'nom_table_pivot' (ex: 'user_role_pivot')
// 'clé_étrangère_utilisateur' (ex: 'mon_user_id' sur la table pivot)
// 'clé_étrangère_rôle' (ex: 'mon_role_id' sur la table pivot)
}
}
// app/Models/Role.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
use HasFactory;
/**
* Obtenir les utilisateurs qui ont ce rôle.
*/
public function users()
{
// Un rôle peut être attribué à plusieurs utilisateurs
return $this->belongsToMany(User::class);
// Laravel déduit automatiquement la table pivot 'role_user' et les clés.
}
}
2.3. Utilisation des Relations belongsToMany
Eloquent offre des méthodes dédiées pour manipuler les relations plusieurs-à-plusieurs, notamment pour attacher, détacher et synchroniser les relations sur la table pivot.
use App\Models\User;
use App\Models\Role;
// Créer des rôles si non existants
$adminRole = Role::firstOrCreate(['name' => 'admin']);
$editorRole = Role::firstOrCreate(['name' => 'editor']);
$viewerRole = Role::firstOrCreate(['name' => 'viewer']);
// Créer un utilisateur
$user = User::create(['name' => 'Bob', 'email' => 'bob@example.com', 'password' => 'secret']);
// Attacher des rôles à l'utilisateur
$user->roles()->attach($adminRole->id); // Attache le rôle 'admin'
$user->roles()->attach($editorRole->id); // Attache le rôle 'editor'
// Vérifier les rôles attachés
echo "Rôles de Bob:\n";
foreach ($user->roles as $role) {
echo " - " . $role->name . "\n";
}
// Détacher un rôle
$user->roles()->detach($editorRole->id); // Détache le rôle 'editor'
// Synchroniser les rôles (détache ceux qui ne sont pas dans la liste, attache ceux qui le sont)
// Ici, Bob n'aura que le rôle 'viewer'
$user->roles()->sync([$viewerRole->id]);
// Ou synchroniser avec des attributs supplémentaires sur la pivot (si la pivot a des colonnes)
// $user->roles()->syncWithPivotValues([$viewerRole->id => ['expires_at' => now()->addYear()]], false);
// Vérifier les rôles après synchronisation
$user->load('roles'); // Recharger la relation
echo "\nRôles de Bob après sync:\n";
foreach ($user->roles as $role) {
echo " - " . $role->name . "\n";
}
// Accéder aux données de la table pivot (si des colonnes ont été ajoutées à la pivot)
// Imaginons que la table pivot 'role_user' ait une colonne 'assigned_date'
// Vous devez spécifier 'withPivot()' dans la définition de la relation
// public function roles() { return $this->belongsToMany(Role::class)->withPivot('assigned_date'); }
// foreach ($user->roles as $role) {
// echo " - " . $role->name . " assigné le: " . $role->pivot->assigned_date . "\n";
// }
III. Les Relations Polymorphiques : morphTo, morphMany, morphToMany
Les relations polymorphiques permettent à un modèle d'appartenir à plusieurs autres modèles sur une seule association. C'est extrêmement utile pour des entités comme les commentaires, les tags ou les images qui peuvent être associées à différents types de ressources.
Exemples concrets :
- Un
Commentairepeut appartenir à unArticleOU à uneVidéoOU à unePhoto. - Un
Tagpeut être associé à unArticleOU à uneVidéoOU à unProduit.
3.1. Relation Polymorphique Un-à-Plusieurs (morphTo, morphMany)
C'est l'équivalent polymorphique de belongsTo et hasMany. Un modèle (l'enfant) peut appartenir à un seul d'un ensemble de plusieurs modèles (les parents polymorphiques), et un modèle parent peut avoir plusieurs enfants polymorphiques.
3.1.1. Modélisation de la Base de Données
La table de l'entité "enfant" (par exemple comments) aura deux colonnes supplémentaires :
{relation}_id: Contient l'ID de l'entité parente.{relation}_type: Contient le nom complet de la classe du modèle parent (ex:App\Models\Post,App\Models\Video).
Prenons l'exemple de Commentaires pour des Articles ou des Vidéos :
-
Table
posts:idtitle- ...
-
Table
videos:idtitle- ...
-
Table
comments:idcontentcommentable_id(ID du parent, peut être l'ID d'un post ou d'une vidéo)commentable_type(Type du parent, ex:App\Models\PostouApp\Models\Video)- ...
3.1.2. Définition des Relations Eloquent
// app/Models/Comment.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
use HasFactory;
protected $fillable = ['content', 'commentable_id', 'commentable_type'];
/**
* Obtenir le parent commentable (post ou video) auquel le commentaire appartient.
*/
public function commentable()
{
// 'commentable' est le nom générique de la relation
return $this->morphTo();
// Syntaxe complète si les colonnes ne sont pas 'commentable_id' et 'commentable_type'
// return $this->morphTo('nomRelation', 'type_colonne', 'id_colonne');
// ex: return $this->morphTo('item', 'item_type', 'item_id');
}
}
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = ['title', 'content'];
/**
* Obtenir tous les commentaires de l'article.
*/
public function comments()
{
// L'article est le parent polymorphique pour les commentaires
return $this->morphMany(Comment::class, 'commentable');
// 'commentable' doit correspondre au nom de la méthode morphTo() dans le modèle Comment
}
}
// app/Models/Video.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Video extends Model
{
use HasFactory;
protected $fillable = ['title', 'url'];
/**
* Obtenir tous les commentaires de la vidéo.
*/
public function comments()
{
// La vidéo est le parent polymorphique pour les commentaires
return $this->morphMany(Comment::class, 'commentable');
}
}
3.1.3. Utilisation des Relations Polymorphiques Un-à-Plusieurs
use App\Models\Post;
use App\Models\Video;
use App\Models\Comment;
// Créer un article et une vidéo
$post = Post::create(['title' => 'Mon article polymorphique', 'content' => 'Contenu...']);
$video = Video::create(['title' => 'Ma vidéo polymorphique', 'url' => 'https://example.com/video']);
// Ajouter des commentaires à l'article
$post->comments()->create(['content' => 'Super article !']);
$post->comments()->create(['content' => 'Très instructif.']);
// Ajouter un commentaire à la vidéo
$video->comments()->create(['content' => 'J\'adore cette vidéo !']);
// Récupérer les commentaires d'un article
$post = Post::find(1);
echo "Commentaires pour l'article '" . $post->title . "':\n";
foreach ($post->comments as $comment) {
echo " - " . $comment->content . "\n";
}
// Récupérer les commentaires d'une vidéo
$video = Video::find(1);
echo "\nCommentaires pour la vidéo '" . $video->title . "':\n";
foreach ($video->comments as $comment) {
echo " - " . $comment->content . "\n";
}
// Accéder au parent depuis un commentaire
$comment = Comment::find(1); // Le premier commentaire
echo "\nLe commentaire '" . $comment->content . "' appartient à: " . $comment->commentable->title . " (" . get_class($comment->commentable) . ")\n";
$comment = Comment::find(3); // Le commentaire de la vidéo
echo "Le commentaire '" . $comment->content . "' appartient à: " . $comment->commentable->title . " (" . get_class($comment->commentable) . ")\n";
3.2. Relation Polymorphique Plusieurs-à-Plusieurs (morphToMany, morphedByMany)
C'est l'équivalent polymorphique de belongsToMany. Elle permet à un modèle d'être lié à plusieurs modèles sur une table pivot, où l'autre côté de la relation peut être plusieurs types de modèles.
3.2.1. Modélisation de la Base de Données
Semblable à la relation belongsToMany, mais la table pivot contient également une colonne {relation}_type pour spécifier le type de modèle.
Prenons l'exemple de Tags pour des Articles ou des Vidéos :
-
Table
posts:idtitle- ...
-
Table
videos:idtitle- ...
-
Table
tags:idname- ...
-
Table pivot
taggables(conventionnellementtaggable_id,taggable_type,tag_id) :tag_id(FOREIGN KEY verstags.id)taggable_id(ID du parent polymorphique)taggable_type(Type du parent polymorphique, ex:App\Models\PostouApp\Models\Video)- ...
3.2.2. Définition des Relations Eloquent
// app/Models/Tag.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
use HasFactory;
protected $fillable = ['name'];
/**
* Obtenir tous les articles qui ont ce tag.
*/
public function posts()
{
// Un tag peut être appliqué à plusieurs articles
return $this->morphedByMany(Post::class, 'taggable');
// 'taggable' est le nom générique de la relation sur la pivot
}
/**
* Obtenir toutes les vidéos qui ont ce tag.
*/
public function videos()
{
// Un tag peut être appliqué à plusieurs vidéos
return $this->morphedByMany(Video::class, 'taggable');
}
}
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = ['title', 'content'];
/**
* Obtenir tous les tags de l'article.
*/
public function tags()
{
// L'article peut avoir plusieurs tags
return $this->morphToMany(Tag::class, 'taggable');
}
}
// app/Models/Video.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Video extends Model
{
use HasFactory;
protected $fillable = ['title', 'url'];
/**
* Obtenir tous les tags de la vidéo.
*/
public function tags()
{
// La vidéo peut avoir plusieurs tags
return $this->morphToMany(Tag::class, 'taggable');
}
}
3.2.3. Utilisation des Relations Polymorphiques Plusieurs-à-Plusieurs
use App\Models\Post;
use App\Models\Video;
use App\Models\Tag;
// Créer des tags
$phpTag = Tag::firstOrCreate(['name' => 'PHP']);
$laravelTag = Tag::firstOrCreate(['name' => 'Laravel']);
$jsTag = Tag::firstOrCreate(['name' => 'JavaScript']);
$devTag = Tag::firstOrCreate(['name' => 'Développement']);
// Créer un article et une vidéo
$post = Post::create(['title' => 'Article sur Laravel', 'content' => 'Contenu Laravel...']);
$video = Video::create(['title' => 'Tutoriel JS', 'url' => 'https://example.com/js-video']);
// Attacher des tags à l'article
$post->tags()->attach($laravelTag->id);
$post->tags()->attach($phpTag->id);
$post->tags()->attach($devTag->id);
// Attacher des tags à la vidéo
$video->tags()->attach($jsTag->id);
$video->tags()->attach($devTag->id);
// Récupérer les tags d'un article
$post = Post::find(1);
echo "Tags de l'article '" . $post->title . "':\n";
foreach ($post->tags as $tag) {
echo " - " . $tag->name . "\n";
}
// Récupérer les tags d'une vidéo
$video = Video::find(1);
echo "\nTags de la vidéo '" . $video->title . "':\n";
foreach ($video->tags as $tag) {
echo " - " . $tag->name . "\n";
}
// Récupérer tous les articles pour un tag donné
$laravelTag = Tag::find(Tag::where('name', 'Laravel')->first()->id);
echo "\nArticles avec le tag '" . $laravelTag->name . "':\n";
foreach ($laravelTag->posts as $post) {
echo " - " . $post->title . "\n";
}
// Récupérer toutes les vidéos pour un tag donné
$jsTag = Tag::find(Tag::where('name', 'JavaScript')->first()->id);
echo "\nVidéos avec le tag '" . $jsTag->name . "':\n";
foreach ($jsTag->videos as $video) {
echo " - " . $video->title . "\n";
}
IV. Astuces et Bonnes Pratiques
- Chargement Paresseux vs. Chargement Eager (Eager Loading) :
- Chargement Paresseux (Lazy Loading) : Par défaut, les relations sont chargées uniquement lorsqu'elles sont accédées. Cela peut entraîner le problème N+1 requêtes (une requête pour le modèle parent, puis N requêtes pour chaque modèle enfant).
- Chargement Eager (Eager Loading) : Utilisez la méthode
with()pour charger les relations en avance, réduisant ainsi le nombre de requêtes à 2 (une pour les parents, une pour les enfants, ou plus si des relations imbriquées sont chargées). C'est essentiel pour les performances.// Problème N+1 (à éviter pour de grandes collections) $users = User::all(); foreach ($users as $user) { echo $user->posts->count(); // Charge les posts pour chaque user individuellement } // Solution Eager Loading $users = User::with('posts')->get(); foreach ($users as $user) { echo $user->posts->count(); // Posts déjà chargés } // Eager loading de relations imbriquées $users = User::with('posts.comments')->get();
- Contraintes sur le Chargement Eager :
Vous pouvez ajouter des contraintes aux requêtes de chargement eager :
$users = User::with(['posts' => function ($query) { $query->where('created_at', '>', now()->subDays(7)); }])->get(); - Lazy Eager Loading :
Si vous avez déjà un modèle et que vous souhaitez charger une relation après l'avoir récupéré (par exemple, si vous ne savez pas si la relation sera nécessaire immédiatement), utilisez la méthode
load():$user = User::find(1); // ... faire quelque chose avec $user sans ses posts $user->load('posts'); // Charge les posts uniquement maintenant - Comptage des Relations :
Pour obtenir le nombre d'éléments liés sans les charger entièrement, utilisez
withCount():$users = User::withCount('posts')->get(); foreach ($users as $user) { echo $user->name . " a " . $user->posts_count . " articles.\n"; } - Existence des Relations (
has,whereHas) : Pour récupérer des modèles basés sur l'existence ou les conditions de leurs relations :// Récupérer les utilisateurs qui ont au moins un article $usersWithPosts = User::has('posts')->get(); // Récupérer les utilisateurs qui ont des articles avec un titre spécifique $usersWithSpecificPosts = User::whereHas('posts', function ($query) { $query->where('title', 'like', '%Laravel%'); })->get();
Conclusion
Les relations Eloquent sont le cœur de la puissance de Laravel en matière d'interaction avec les bases de données. La maîtrise des relations hasMany, belongsToMany et des relations polymorphiques (morphTo, morphMany, morphToMany) est indispensable pour concevoir des schémas de base de données sophistiqués et écrire du code clair, maintenable et performant.
En comprenant quand utiliser chaque type de relation et comment les implémenter correctement, vous serez en mesure de modéliser des scénarios complexes avec élégance, d'éviter les problèmes de performance courants (comme le problème N+1) et de rendre votre code beaucoup plus expressif et agréable à travailler. Continuez à pratiquer en créant différents types de relations dans vos projets pour solidifier vos connaissances.