Introduction aux relations polymorphes d’Eloquent

Vous avez probablement utilisé différents types de relations entre des modèles ou des tables de base de données, comme celles que l’on voit couramment dans Laravel : one-to-one, one-to-many, many-to-many et has-many-through. Mais il existe un autre type de relation qui n’est pas si courant : la relation polymorphe. Qu’est-ce qu’une relation polymorphe ?

Une relation polymorphe est une relation dans laquelle un modèle peut appartenir à plus d’un autre modèle sur une seule association.

Pour clarifier ce point, créons une situation imaginaire dans laquelle nous avons un modèle Topic et un Post modèle. Les utilisateurs peuvent laisser des commentaires à la fois sur les sujets et les messages. En utilisant les relations polymorphes, nous pouvons utiliser un seul modèle comments pour ces deux scénarios. Surprenant, non ? Cela ne semble pas très pratique puisque, dans l’idéal, nous devrions créer une table post_comments et une table topic_comments pour différencier les commentaires. Avec les relations polymorphes, nous n’avons pas besoin de deux tables. Examinons les relations polymorphes à travers un exemple pratique.

Ce que nous allons construire

Nous allons créer une application musicale de démonstration qui contient des chansons et des albums. Dans cette application, nous aurons la possibilité de voter pour les chansons et les albums. En utilisant des relations polymorphes, nous utiliserons une seule table upvotes pour ces deux scénarios. Tout d’abord, examinons la structure de la table requise pour construire cette relation :

albums
    id - integer
    name - string

songs
    id - integer
    title - string
    album_id - integer

upvotes
    id - integer
    upvoteable_id - integer
    upvoteable_type - string

Parlons de la table upvoteable_id et upvoteable_type qui peuvent sembler un peu étrangères à ceux qui n’ont jamais utilisé de relations polymorphes auparavant. Le site upvoteable_id contiendra la valeur d’identification de l’album ou de la chanson, tandis que la colonneupvoteable_idcontient la valeur d’identification de l’album ou de la chanson. upvoteable_type contiendra le nom de la classe du modèle propriétaire. Le site upvoteable_type est la façon dont l’ORM détermine le « type » de modèle propriétaire à retourner lors de l’accès à l’élément de données de l’ORM. upvoteable relation.

Générer les modèles en même temps que les migrations

Je suppose que vous avez déjà une application Laravel en cours d’exécution. Commençons par créer les trois modèles et les migrations, puis modifions les migrations en fonction de nos besoins.

php artisan make:model Album -m
php artisan make:model Song -m
php artisan make:model Upvote -m

Notez que passer l’option -m lors de la création de modèles générera également les migrations associées à ces modèles. Modifions le paramètre up dans ces migrations pour obtenir la structure de table souhaitée :

{timestamp}_create_albums_table.php

public function up()
    {
        Schema::create('albums', function (Blueprint $table) {
           $table->increments('id');
            $table->string('name');
            $table->timestamps();
        });
    }

{timestamp}_create_songs_table.php

public function up()
    {
        Schema::create('songs', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->integer('album_id')->unsigned()->index();
            $table->timestamps();

            $table->foreign('album_id')->references('id')->on('album')->onDelete('cascade');
        });
    }

{timestamp}_create_upvotes_table.php

public function up()
    {
        Schema::create('upvotes', function (Blueprint $table) {
            $table->increments('id');
            $table->morphs('upvoteable'); // Adds unsigned INTEGER upvoteable_id and STRING upvoteable_type
            $table->timestamps();
        });
    }

Nous pouvons maintenant lancer l’artisan migrate pour créer les trois tables :

php artisan migrate

Configurons maintenant nos modèles pour prendre note de la relation polymorphe entre les albums, les chansons et les votes positifs :

app/Upvote.php

[...]
class Upvote extends Model
{
    /**
     * Get all of the owning models.
     */
    public function upvoteable()
    {
        return $this->morphTo();
    }
}

app/Album.php

class Album extends Model
{
    protected $fillable = ['name'];

    public function songs()
    {
        return $this->hasMany(Song::class);
    }

    public function upvotes()
    {
        return $this->morphMany(Upvote::class, 'upvoteable');
    }
}

app/Song.php

class Song extends Model
{
    protected $fillable = ['title', 'album_id'];

    public function album()
    {
        return $this->belongsTo(Album::class);
    }

    public function upvotes()
    {
        return $this->morphMany(Upvote::class, 'upvoteable');
    }
}

The upvotes dans les deux modèles Album et Song définissent une relation polymorphe de type un-à-plusieurs entre ces modèles et le modèle Upvote et nous aidera à obtenir tous les votes positifs pour une instance de ce modèle particulier.

Avec les relations définies, nous pouvons maintenant jouer avec l’application afin de mieux comprendre le fonctionnement des relations polymorphes. Nous ne créerons pas de vues pour cette application, nous allons juste jouer avec notre application depuis la console.

Au cas où vous penseriez à des contrôleurs et à l’endroit où nous devrions placer la balise upvote je vous suggère de créer une méthode AlbumUpvoteController et un SongUpvoteController. De cette façon, nous gardons les choses strictement liées à l’objet sur lequel nous agissons lorsque nous travaillons avec des relations polymorphes. Dans notre cas, nous pouvons voter pour des albums et des chansons. La note positive ne fait pas partie d’un album ni d’une chanson. De plus, il ne s’agit pas d’un vote positif général, contrairement à ce qui se passerait si nous avions une relation UpvotesController dans la plupart des relations un-à-plusieurs. J’espère que cela a du sens.

Allumons la console :

php artisan tinker
>>> $album = App\Album::create(['name' => 'More Life']);
>>> $song = App\Song::create(['title' => 'Free smoke', 'album_id' => 1]);
>>> $upvote1 = new App\Upvote;
>>> $upvote2 = new App\Upvote;
>>> $upvote3 = new App\Upvote;
>>> $album->upvotes()->save($upvote1)
>>> $song->upvotes()->save($upvote2)
>>> $album->upvotes()->save($upvote3)

Récupération des relations

Maintenant que nous avons des données en place, nous pouvons accéder à nos relations via nos modèles. Vous trouverez ci-dessous une capture d’écran des données contenues dans le modèle tableau des votes positifs:

Tableau des Upvotes

Pour accéder à toutes les notes positives d’un album, nous pouvons utiliser la propriété dynamique upvotes :

$album = App\Album::find(1);
$upvotes = $album->upvotes;
$upvotescount = $album->upvotes->count();

Il est également possible de récupérer le propriétaire d’une relation polymorphe à partir du modèle polymorphe en accédant au nom de la méthode qui effectue l’appel à la fonction morphTo. Dans notre cas, il s’agit de la méthode upvoteable sur le modèle Upvote. Nous allons donc accéder à cette méthode comme une propriété dynamique :

$upvote = App\Upvote::find(1);
$model = $upvote->upvoteable;

Le site upvoteable sur le modèle Upvote renverra une relation Album puisque cette upvote appartient à une instance du modèle Album instance.

Comme il est possible d’obtenir le nombre de votes positifs pour une chanson ou un album, nous pouvons trier les chansons ou les albums en fonction des votes positifs dans une vue. C’est ce qui se passe dans les classements musicaux.

Dans le cas d’une chanson, nous obtiendrions les votes positifs de la manière suivante :

$song = App\Song::find(1);
$upvotes = $song->upvotes;
$upvotescount = $song->upvotes->count();

Types polymorphes personnalisés

Par défaut, Laravel utilise le nom de classe entièrement qualifié pour stocker le type du modèle associé. Par exemple, dans l’exemple ci-dessus, un modèle Upvote peut appartenir à un modèle Album ou à un Song, la valeur par défaut est upvoteable_type serait soit App\Album soit App\Song respectivement.

Cependant, il y a un gros défaut à cela. Que se passe-t-il si l’espace de nom du Album change ? Nous devrons effectuer une sorte de migration pour renommer toutes les occurrences dans le modèle upvotes . Et ça, c’est un peu rusé ! Que se passe-t-il également dans le cas d’espaces de noms longs (tels que App\Models\Data\Topics\Something\SomethingElse) ? Cela signifie que nous devons définir une longue longueur maximale pour la colonne. Et c’est là que le MorphMap vient à notre secours.

La méthode « morphMap » demandera à Eloquent d’utiliser un nom personnalisé pour chaque modèle au lieu du nom de la classe :

use IlluminateDatabaseEloquentRelationsRelation;

Relation::morphMap([
    'album' => AppAlbum::class,
    'song' => AppSong::class,
]);

Nous pouvons enregistrer la méthode morphMap dans la fonction de démarrage de notre AppServiceProvider ou créer un fournisseur de services distinct. Pour que les nouveaux changements prennent effet, nous devons exécuter la commande composer dump-autoload . Donc maintenant, nous pouvons ajouter ce nouvel enregistrement de vote positif :

[
    "id" => 4,
    "upvoteable_type" => "album",
    "upvoteable_id" => 1
]

et il se comportera exactement de la même manière que l’exemple précédent.

Conclusion

Même si vous n’avez probablement jamais été confronté à une situation nécessitant l’utilisation de relations polymorphes, ce jour viendra probablement un jour. L’avantage de travailler avec Laravel, c’est qu’il est très facile de faire face à cette situation sans avoir à recourir à un quelconque artifice d’association de modèle pour que les choses fonctionnent. Laravel prend même en charge les relations polymorphes many-to-many. Vous pouvez en savoir plus à ce sujet ici.

J’espère que vous avez maintenant compris les relations polymorphes et les situations qui peuvent faire appel à ce type de relations.

Nouveau Tutoriel

Newsletter

Ne manquez jamais les nouveaux conseils, tutoriels et autres.

Pas de spam, jamais. Nous ne partagerons jamais votre adresse électronique et vous pouvez vous désabonner à tout moment.