Parmi toutes les relations proposées par Laravel, les relations polymorphiques sont probablement les plus complexes et exotiques à mettre en place.
Dans ce tutoriel, démystifions les relations morphMany
et morphOne
à l'aide d'un exercice pratique !
- Prérequis
- Pourquoi utiliser un polymorphe
- Les bases
- Trier les relations
- Quelques relations plus complexes
- Depuis les factory
- Customiser la relation
Prérequis
Avant de poursuivre ce tutoriel nous allons créer les migrations, les modèles et les factories nécessaires au bon fonctionnement de notre relation polymorphique à l'aide des commandes suivantes :
1php artisan make:model Post --migration --factory2php artisan make:model Comment --migration --factory3php artisan make:model Image --migration --factory
Deux colonnes supplémentaires seront également nécessaires à notre table images
, nous verrons leurs importances un peu plus tard :
1Schema::create('images', function (Blueprint $table) {2 $table->id();3 $table->integer('imageable_id'); 4 $table->string('imageable_type'); 5 $table->timestamps();6});
Nous allons désormais ajouter dans nos modèles Post
et Comment
une relation morphMany
dénommée images
:
1class Post extends Model 2{ 3 use HasFactory; 4 5 public function images() 6 { 7 return $this->morphMany(Image::class, 'imageable'); 8 } 9}10 11class Comment extends Model12{13 use HasFactory;14 15 public function images() 16 { 17 return $this->morphMany(Image::class, 'imageable'); 18 } 19}
Pour finir, ajoutons la relation imageable
à notre modèle Image
, cette dernière contiendra le morphTo
faisant lien avec nos deux précédents modèles :
1namespace App\Models; 2 3use Illuminate\Database\Eloquent\Factories\HasFactory; 4use Illuminate\Database\Eloquent\Model; 5 6class Image extends Model 7{ 8 use HasFactory; 9 10 public function imageable()11 {12 return $this->morphTo();13 }14}
Votre application est désormais prête à utiliser une relation polymorphique !
Pourquoi utiliser un polymorphe
Dans le cadre de ce tutoriel, nous possédons un modèle Image
pouvant être lié à l'aide d'un polymorphe soit à un model Post
ou alors à un model Comment
:
Dans cette situation, les modèles Post
et Comment
possèdent des images, cette relation est polymorphique car la ligne insérée dans la table images
conserve un lien vers le model associé :
Une relation polymorphique offre une certaine souplesse dans votre structure de données, il sera possible de créer un nouveau modèle possédant une image sans avoir à retoucher à la structure de votre base de données, le modèle Image
pourra facilement accueillir un nouveau modèle grâce à son polymorphisme.
Le second intérêt d'une relation polymorphique réside dans la possibilité de lier plusieurs images à un même modèle sans avoir à créer de table intermédiaire, comme par exemple dans le cadre d'une relation belongsToMany.
Pour résumer, grace à la relation polymorphique :
- Un modèle
Post
ouComment
peut facilement posséder une ou plusieurs images. - Un modèle
Image
peut facilement être lié à n'importe quel modèle implémentant le bonmorphTo
.
Un polymorphe permet de lier un modèle à n'importe quel autre modèle
Les bases du morphMany
Maintenant que notre relation polymorphique est fonctionnelle et que nous en savons un peu plus sur ce type de relation, commençons à manipuler notre morphMany
:
1$post = Post::create();2 3$post->images()->save(Image::make());4$post->images()->save(Image::make());5$post->images()->save(Image::make());
Une fois associée à un post, les images seront par la suite facilement accessibles depuis la relation images
du modèle :
1$post = Post::first();2 3$image = $post->images()->first();
Dans le sens inverse, vous pourrez récupérer le modèle associé à une image depuis la relation imageable
, contenant le morphTo
, que nous avons créé précédemment :
1$image = Image::first();2 3$post = $image->imageable;
Trier les relations
À l'instar d'autres relations du framework, il est envisageable de trier une relation polymorphique à l'aide des méthodes oldestOfMany
et latestOfMany
.
Pour ce faire, ajoutons les deux méthodes suivantes dans notre modèle Post
:
1class Post extends Model 2{ 3 use HasFactory; 4 5 public function images() 6 { 7 return $this->morphMany(Image::class, 'imageable'); 8 } 9 10 public function oldestImage() 11 { 12 return $this->morphOne(Image::class, 'imageable')->oldestOfMany(); 13 } 14 15 public function latestImage() 16 { 17 return $this->morphOne(Image::class, 'imageable')->latestOfMany(); 18 } 19}
Petite particularité, l'utilisation de ces méthodes de tri nécessite l'ajout d'un morphOne
à la place du morphMany
, vous pourrez dès lors facilement récupérer les bonnes images :
1$post = Post::first();2 3$oldestImage = $post->oldestImage;4$latestImage = $post->latestImage;
Attention, par défaut ce tri s'effectue sur la colonne id
de votre modèle et non, comme on pourrait l'imaginer, sur la valeur de la colonne created_at
. Vous trouverez le code associé à ces deux méthodes dans la classe CanBeOneOfMany du framework :
1public function oldestOfMany($column = 'id', $relation = null)2{3 return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) {4 return [$column => 'MIN'];5 })->all(), 'MIN', $relation);6}
Ce comportement pourrait devenir problématique si la colonne id
de votre modèle n'est pas auto incrémentée par votre engine de base de données, vous pourrez modifier ce comportement en précisant la colonne à utiliser pour effectuer le tri directement depuis la méthode :
1->oldestOfMany('created_at');2->latestOfMany('created_at');
Voyons désormais comment effectuer des requêtes plus complexes à partir d'une relation polymorphique.
Quelques relations plus complexes
Pour commencer, modifions la migration de notre table images
en y ajoutant une colonne likes
de type integer :
1Schema::create('images', function (Blueprint $table) {2 $table->id();3 $table->integer('imageable_id');4 $table->string('imageable_type');5 $table->integer('likes')->default(0); 6 $table->timestamps();7});
N'oubliez pas de modifier la factory et le modèle en conséquence !
Imaginons que nous souhaitions récupérer l'image ayant le plus de "likes" d'un post, pour ce faire nous pouvons utiliser un morphOne
couplé à un ofMany
afin de réaliser la requête dans une nouvelle méthode mostLikedImage
:
1class Post extends Model 2{ 3 use HasFactory; 4 5 public function images() 6 { 7 return $this->morphMany(Image::class, 'imageable'); 8 } 9 10 public function mostLikedImage() 11 { 12 return $this->morphOne(Image::class, 'imageable')->ofMany([ 13 'likes' => 'max', 14 ]); 15 } 16}
La méthode ofMany
accepte en premier argument un tableau contenant le nom de la colonne sur laquelle effectuer le tri ainsi qu'une fonction d'agrégation, à ce jour, seules les fonctions min
et max
sont utilisables.
Comme bien souvent dans Laravel, le framework cherchera à être explicite et relativement verbeux dans ses conventions de nommage : "morph one of many".
1$post = Post::first();2 3$image = $post->mostLikedImage;
Le second argument de la méthode ofMany
accepte quant à lui une closure recevant une instance de $query
, vous pourrez ainsi modifier en profondeur la requête exécutée par la relation.
Par exemple, si nous souhaitons récupérer l'image ayant le plus de likes sur la dernière semaine, nous pouvons modifier la requête de la manière suivante :
1class Post extends Model 2{ 3 use HasFactory; 4 5 public function images() 6 { 7 return $this->morphMany(Image::class, 'imageable'); 8 } 9 10 public function mostLikedImage()11 {12 return $this->morphOne(Image::class, 'imageable')->ofMany([13 'likes' => 'max',14 ], function ($query) { 15 return $query->where('created_at', '>', now()->subWeek()); 16 }); 17 }18}
Depuis les factory
Une relation polymorphique peut, au premier abord, sembler complexe à créer depuis une factory, un simple each
vous permettra de créer facilement les relations vers notre modèle Image
:
1Post::factory()->create()->each(function (Post $post) { 2 3 $images = Image::factory()->count(3)->make(); 4 5 $post->images()->saveMany($images); 6}); 7 8Comment::factory()->create()->each(function (Comment $comment) { 9 10 $images = Image::factory()->count(3)->make();11 12 $comment->images()->saveMany($images);13});
Customiser la relation
Dans le cadre d'une relation polymorphique, Laravel conservera le Fully Qualified Class Name (FQCN) de la classe dans le champ imageable_type
que nous avons créé précédemment dans ce tutoriel.
En d'autres termes, cela signifie que le namespace complet de vos classes apparaîtra dans votre base de données et provoquera un couplage malencontreux entre votre code et votre BDD.
Ce comportement par défaut peut paraitre anodin mais constitue un élément structurant de votre application que l'on pourrait négliger lors de la mise en place de nos relations.
Une base de données est un composant de votre application évoluant à un rythme différent de votre code, votre base de données ne devrait pas avoir connaissance de la structure de votre code, cela n'est pas de sa responsabilité.
En laissant ce comportement par défaut, vous vous retrouverez un jour ou l'autre dans l'embarras quand vous déciderez de changer le nom ou le namespace d'un modèle, il devient alors essentiel de garder le contrôle sur vos relations.
Heureusement pour nous, Laravel nous autorise à configurer ce comportement en nommant nous-même la valeur à sauvegarder pour chaque modèle dans une relation polymorphique :
1Relation::enforceMorphMap([2 'post' => Post::class,3 'comment' => Comment::class,4]);
Après cette configuration, la valeur sauvegardée en base de données ne sera plus App\Models\Post
mais uniquement post
, la base de données n'aura plus conscience de la structure de votre code et votre application pourra évoluer plus sereinement !
A lire
Autres articles de la même catégorie
Le pattern Pipeline
Laravel dispose d'un puissant service de Pipeline méconnu de la plupart des développeurs, explorons ensemble les possibilités que propose ce pattern !
Mathieu De Gracia
Optimisez votre application avec le chunk de Laravel
Voyons ce que propose Laravel pour traiter de grandes quantités de données efficacement
Mathieu De Gracia
Pourquoi Vite ne build pas ?
Un retour d'experience douloureux mais formateur sur le fonctionnement de Vite !
Mathieu De Gracia