Il y a souvent un moment dans la vie de tout développeur où vous devez interagir avec une base de données. C’est là qu’Eloquent, le mappeur objet-relationnel (Object-Relational Mapper ou ORM) de Laravel, rend le processus d’interaction avec les tables de votre base de données intuitif et naturel.

En tant que professionnel, il est essentiel que vous reconnaissiez et compreniez les six types de relations clés que nous allons passer en revue.

Que sont les relations dans Eloquent ?

Lorsque vous travaillez avec des tables dans une base de données relationnelle, nous pouvons caractériser les relations comme des connexions entre les tables. Cela vous aide à organiser et à structurer les données sans effort, ce qui permet une meilleure lisibilité et une meilleure manipulation des données. En pratique, il existe trois types de relations dans les bases de données :

  • une à une – Un enregistrement dans une table est associé à une, et une seule, dans une autre table. Par exemple, une personne et un numéro de sécurité sociale.
  • une-à-plusieurs – Un enregistrement est associé à plusieurs enregistrements dans une autre table. Par exemple, un écrivain et ses blogs.
  • plusieurs à plusieurs – Plusieurs enregistrements d’une table sont associés à plusieurs enregistrements d’une autre table. Par exemple, les étudiants et les cours auxquels ils sont inscrits.

Laravel permet d’interagir et de gérer les relations entre les bases de données en utilisant la syntaxe orientée objet d’Eloquent.

En plus de ces définitions, Laravel introduit d’autres relations, à savoir :

  • Has Many Through
  • Relations polymorphes
  • Plusieurs-à-plusieurs Polymorphes

Prenons l’exemple d’un magasin dont l’inventaire contient une variété d’articles, chacun dans sa propre catégorie. Il est donc logique, d’un point de vue commercial, de diviser la base de données en plusieurs tables. Cela pose des problèmes, car vous ne souhaitez pas interroger chaque table.

Nous pouvons facilement créer une simple relation un-à-plusieurs dans Laravel pour nous aider, par exemple lorsque nous avons besoin d’interroger les produits, nous pouvons le faire en utilisant le modèle Product.

Schéma de base de données avec trois tables et une table commune représentant une relation polymorphe
Schéma de base de données avec trois tables et une table commune représentant une relation polymorphe

Relation un-à-un

Première relation de base proposée par Laravel, elle associe deux tables de manière à ce qu’une ligne de la première table soit corrélée à une seule ligne de l’autre table.

Pour voir cela en action, nous devons créer deux modèles avec leur propre migration :

php artisan make:model Tenant 
Php artisan make:model Rent

À ce stade, nous avons deux modèles, l’un étant le locataire et l’autre son loyer.

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * Get the rent of a Tenant
    */
    public function rent() 
    {
        return $this->hasOne(Rent::class);
    }
}

Comme Eloquent détermine la relation de clé étrangère en se basant sur le nom du modèle parent (Tenant dans ce cas), le modèle Rent suppose qu’il existe une clé étrangère tenant_id.

Nous pouvons facilement l’écraser en ajoutant un argument supplémentaire à la méthode hasOne:

return $this- >hasOne(Rent::class, "custom_key");

Eloquent suppose également qu’il existe une correspondance entre la clé étrangère définie et la clé primaire du parent (modèle Tenant). Par défaut, il cherchera à faire correspondre tenant_id avec la clé id de l’enregistrement Tenant. Nous pouvons remplacer cela par un troisième argument dans la méthode hasOne, de manière à ce qu’il corresponde à une autre clé :

return $this->hasOne(Rent::class, "custom_key", "other_key"); 

Maintenant que nous avons défini la relation biunivoque entre les modèles, nous pouvons l’utiliser facilement, comme ci-dessous :

$rent = Tenant::find(10)->rent;

Avec cette ligne de code, nous obtenons le loyer du locataire avec l’identifiant 10 s’il existe.

Relation d’un à plusieurs

Comme la relation précédente, celle-ci définira les relations entre un modèle monoparental et plusieurs modèles enfants. Il est peu probable que notre locataire n’ait qu’une seule facture de loyer, car il s’agit d’un paiement récurrent ; il aura donc plusieurs paiements.

Dans ce cas, notre relation précédente présente des défauts et nous pouvons les corriger :

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * Get the rents of a Tenant
    */
    public function rent() 
    {
        return $this->hasMany(Rent::class);
    }
}

Avant d’appeler la méthode pour obtenir les loyers, il est bon de savoir que les relations servent à construire des requêtes, de sorte que nous pouvons ajouter des contraintes (comme un loyer entre deux dates, un paiement minimum, etc :

$rents = Tenant::find(10)->rent()->where('payment', '>', 500)->first();

Et comme pour la relation précédente, nous pouvons écraser les clés étrangères et locales en passant des arguments supplémentaires :

return $this->hasMany(Rent::class, "foreign_key");
return $this->hasMany(Rent::class, "foreign_key", "local_key");

Nous disposons désormais de tous les loyers d’un locataire, mais que faire lorsque nous connaissons le loyer et que nous voulons savoir à qui il appartient ? Nous pouvons utiliser la propriété belongsTo :

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * Return the tenant for the rent
    */
    public function tenant() 
    {
        return $this->belongsTo(Tenant::class);
    }
}

Et maintenant, nous pouvons facilement trouver le locataire :

$tenant = Rent::find(1)->tenant;

Pour la méthode belongsTo, nous pouvons également écraser les clés étrangères et locales comme nous l’avons fait précédemment.

Relation de type un sur plusieurs (Has-One-Of-Many)

Étant donné que notre modèle Locataire peut être associé à de nombreux modèles Location, nous voulons facilement récupérer le modèle le plus récent ou le plus ancien des relations.

Une façon pratique de le faire est de combiner les méthodes hasOne et ofMany :

public function latestRent() {
    return $this->hasOne(Rent::class)->latestOfMany();
}

public function oldestRent() {
    return $this->hasOne(Rent::class)->oldestOfMany();
}

Par défaut, nous récupérons les données sur la base de la clé primaire, qui peut être triée, mais nous pouvons créer nos propres filtres pour la méthode ofMany :

return $this->hasOne(Rent::class)->ofMany('price', 'min');

Relations HasOneThrough et HasManyThrough

Les méthodes -Through suggèrent que nos modèles devront passer par un autre modèle pour établir une relation avec le modèle souhaité. Par exemple, nous pouvons associer le loyer au propriétaire, mais le loyer doit d’abord passer par le locataire pour atteindre le propriétaire.

Les clés des tables nécessaires à cet effet ressembleraient à ceci :

rent
    id - integer
    name - string
    value - double

tenants
    id - integer
    name - string
    rent_id - integer

landlord
    id - integer
    name - string
    tenant_id - integer

Après avoir visualisé l’aspect de nos tables, nous pouvons créer les modèles :

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * Return the rents' landlord
    */
    public function rentLandlord() 
    {
        return $this->hasOneThrough(Landlord::class, Tenant::class);
    }
}

Le premier argument de la méthode hasOneThrough est le modèle auquel vous voulez accéder, et le deuxième argument est le modèle que vous allez traverser.

Et comme précédemment, vous pouvez écraser les clés étrangères et locales. Maintenant que nous avons deux modèles, nous en avons deux de chaque à écraser dans cet ordre :

public function rentLandlord() 
{
    return $this->hasOneThrough(
        Landlord::class,
        Tenant::class,
        "rent_id",    // Foreign key on the tenant table
        "tenant_id",  // Foreign key on the landlord table
        "id",         // Local key on the tenant class
        "id"          // Local key on the tenant table
    );
}

De même, la relation « Has Many Through » de Laravel Eloquent est utile lorsque vous souhaitez accéder aux enregistrements d’une table distante par le biais d’une table intermédiaire. Prenons un exemple avec trois tables :

  • country
  • users
  • games

Chaque pays a plusieurs utilisateurs et chaque utilisateur a plusieurs jeux. Nous voulons récupérer tous les jeux appartenant à un pays dans la table users

Vous devez définir les tables comme suit :

country
    id - integer
    name - string

user
    id - integer
    country_id - integer
    name - string

games
    id - integer
    user_id - integer
    title - string

Vous devez maintenant définir le modèle Eloquent pour chaque table :

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

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

    public function users()
    {
        return $this->hasMany(User::class);
    }

    public function games()
    {
        return $this->hasManyThrough(Games::class, User::class);
    }
}
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = [article_id, 'name'];

    public function country()
    {
        return $this->belongsTo(Country::class);
    }

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Game extends Model
{
    protected $fillable = ['user_id', 'title'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Nous pouvons maintenant appeler la méthode games() du modèle Pays pour obtenir tous les jeux, car nous avons établi la relation « Has Many Through » entre Pays et Jeu par l’intermédiaire du modèle Utilisateur.

<?php

$country = Country::find(159);
            
// Retrieve all games for the country
$games = $country->games;

Relation de plusieurs à plusieurs

La relation de plusieurs à plusieurs est plus compliquée. Un bon exemple est celui d’un employé qui a plusieurs rôles. Un rôle peut également être attribué à plusieurs employés. C’est la base de la relation de plusieurs à plusieurs.

Pour cela, nous devons disposer des tables employees, roles et role_employees .

La structure des tables de notre base de données se présente comme suit :

employees
    id - integer
    name - string

roles 
    id - integer
    name - string

role_employees
    user_id - integer
    role_id - integer

En connaissant la structure des tables de la relation, nous pouvons facilement définir notre modèle Employé comme appartenant à plusieurs modèles Rôle.

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Employee extends Model
{
    public function roles() 
    {
        return $this- >belongsToMany(Role::class);
    }
}

Une fois cette définition effectuée, nous pouvons accéder à tous les rôles d’un employé et même les filtrer :

$employee = Employee::find(1);
$employee->roles->forEach(function($role) { // });

// OR 

$employee = Employee::find(1)->roles()->orderBy('name')->where('name', 'admin')->get();

Comme toutes les autres méthodes, nous pouvons écraser les clés étrangères et locales de la méthode belongsToMany.

Pour définir la relation inverse de la méthode belongsToMany, il suffit d’utiliser la même méthode, mais sur la méthode enfant, avec le parent comme argument.

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    public function employees() 
    {
        return $this->belongsToMany(Employee::class);
    }
}

Utilisations de la table intermédiaire

Comme nous l’avons peut-être remarqué, lorsque nous utilisons la relation plusieurs-à-plusieurs, nous sommes toujours censés avoir une table intermédiaire. Dans ce cas, nous utilisons la table role_employees.

Par défaut, notre tableau croisé dynamique ne contient que les attributs id. Si nous voulons d’autres attributs, nous devons les spécifier comme suit :

return $this->belongsToMany(Employee::class)->withPivot("active", "created_at");

Si nous voulons raccourcir le tableau croisé dynamique pour les horodatages, nous pouvons le faire :

return $this->belongsToMany(Employee::class)->withTimestamps();

Il faut savoir que nous pouvons personnaliser le nom « pivot » pour qu’il corresponde mieux à notre application :

return $this->belongsToMany(Employee::class)->as('subscription')->withPivot("active", "created_by");

Filtrer les résultats d’une requête éloquente est un must pour tout développeur qui souhaite améliorer son jeu et optimiser ses applications Laravel.

C’est pourquoi Laravel propose une fonctionnalité fantastique, les pivots, qui peuvent être utilisés pour filtrer les données que nous voulons collecter. Ainsi, au lieu d’utiliser d’autres fonctionnalités comme les transactions de base de données pour obtenir nos données en morceaux, nous pouvons les filtrer avec des méthodes utiles comme wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNull, wherePivotNotNull et nous pouvons les utiliser lors de la définition des relations entre les tables !

return $this->belongsToMany(Employee::class)->wherePivot('promoted', 1);
return $this->belongsToMany(Employee::class)->wherePivotIn('level', [1, 2]);
return $this->belongsToMany(Employee::class)->wherePivotNotIn('level', [2, 3]);
return $this->belongsToMany(Employee::class)->wherePivotBetween('posted_at', ['2023-01-01 00:00:00', '2023-01-02 00:00:00']);
return $this->belongsToMany(Employee::class)->wherePivotNull('expired_at');
return $this->belongsToMany(Employee::class)->wherePivotNotNull('posted_at');

Une dernière fonctionnalité étonnante est la possibilité de classer les pivots :

return $this->belongsToMany(Employee::class)
        ->where('promoted', true)
        ->orderByPivot('hired_at', 'desc');

Relations polymorphes

Le mot « polymorphe » vient du grec et signifie « plusieurs formes » Ainsi, un modèle dans notre application peut prendre plusieurs formes, ce qui signifie qu’il peut avoir plus d’une association. Imaginez que nous construisions une application avec des blogs, des vidéos, des sondages, etc. Un utilisateur peut créer un commentaire pour chacun de ces éléments. Un utilisateur peut créer un commentaire pour n’importe lequel de ces éléments. Par conséquent, un modèle Commentaire peut appartenir aux modèles Blogs, Vidéos et Sondages.

Polymorphe un à un

Ce type de relation est similaire à une relation univoque standard. La seule différence est que le modèle enfant peut appartenir à plusieurs types de modèles avec une seule association.

Prenez, par exemple, un modèle Locataire et Propriétaire, il peut partager une relation polymorphe avec un modèle Facture d’eau.

La structure de la table peut être la suivante :

tenants
    id – integer
    name – string

landlords
    id – integer
    name – string

waterbills
    id – integer
    amount – double
    waterbillable_id
    waterbillable_type

Nous utilisons waterbillable_id pour l’identifiant du propriétaire ou du locataire, tandis que waterbillable_type contient le nom de la classe du modèle parent. La colonne type est utilisée par Eloquent pour déterminer le modèle parent à retourner.

La définition du modèle pour une telle relation se présente comme suit :

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class WaterBill extends Model
{
    public function billable()
    {
        return $this->morphTo();
    }
}

class Tenant extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

class Landlord extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

Une fois que tout cela est en place, nous pouvons accéder aux données du modèle du propriétaire et du locataire :

<?php

$tenant = Tenant::find(1)->waterBill;
$landlord = Landlord::find(1)->waterBill;

Une relation polymorphe de type un à plusieurs

Il s’agit d’une relation similaire à une relation classique de type « un à plusieurs », la seule différence essentielle étant que le modèle enfant peut appartenir à plusieurs types de modèles, en utilisant une seule association.

Dans une application comme Facebook, les utilisateurs peuvent commenter des messages, des vidéos, des sondages, des événements en direct, etc. Avec une association polymorphe, nous pouvons utiliser une seule table de commentaires pour stocker les commentaires de toutes les catégories que nous avons. La structure de notre table ressemblerait à ceci :

posts 
    id – integer
    title – string
    body – text

videos
    id – integer
    title – string
    url – string

polls
    id – integer
    title – string

comments 
    id – integer
    body – text
    commentable_id – integer
    commentable_type – string

Le commentable_id étant l’identifiant de l’enregistrement, et le commentable_type étant le type de classe, de sorte qu’eloquent sache ce qu’il doit chercher. Quant à la structure du modèle, elle est très similaire à la structure polymorphe one-to-many :

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model 
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Poll extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Live extends Model
{
    public function comments()
    {
        return $this->morphMany(Comments::class, 'commentable');
    }
}

Maintenant, pour récupérer les commentaires d’un Live, nous pouvons simplement appeler la méthode find avec l’id, et nous avons maintenant accès à la classe itérable comments :

<?php

use App\Models\Live;

$live = Live::find(1);

foreach ($live->comments as $comment) { }

// OR

Live::find(1)->comments()->each(function($comment) { // });
Live::find(1)->comments()->map(function($comment) { // });
Live::find(1)->comments()->filter(function($comment) { // });

// etc.

Et si nous avons le commentaire et que nous voulons savoir à qui il appartient, nous accédons à la méthode commentable :

<?php

use App\Models\Comment;

$comment = Comment::find(10);
$commentable = $comment->commentable;

// commentable – type of Post, Video, Poll, Live

Polymorphe un à d’autres

Dans de nombreuses applications qui évoluent, nous voulons un moyen facile d’interagir avec les modèles et entre eux. Nous pouvons vouloir connaître le premier ou le dernier message d’un utilisateur, ce qui peut être fait avec une combinaison de méthodes morphOne et ofMany :

<?php

public function latestPost()
{
    return $this->morphOne(Post::class, 'postable')->latestOfMany();
}

public function oldestPost()
{
    return $this->morphOne(Post::class, 'postable')->oldestOfMany();
}

Les méthodes latestOfMany et oldestOfMany récupèrent le modèle le plus récent ou le plus ancien en fonction de la clé primaire du modèle, qui était la condition pour qu’il puisse être trié.

Dans certains cas, nous ne voulons pas trier par l’ID, peut-être avons-nous changé la date de publication de certains articles et nous les voulons dans cet ordre, pas par leur ID.

Pour ce faire, vous pouvez passer deux paramètres à la méthode ofMany. Le premier paramètre est la clé par laquelle nous voulons filtrer, et le second est la méthode de tri :

<?php

public function latestPublishedPost()
{
    return $this->morphOne(Post::class, "postable")->ofMany("published_at", "max");
}

En gardant cela à l’esprit, il est possible de construire des relations plus avancées pour cela ! Imaginez le scénario suivant. On nous demande de générer une liste de tous les articles actuels dans l’ordre dans lequel ils ont été publiés. Le problème se pose lorsque deux articles ont la même valeur published_at et lorsque des articles sont programmés pour être publiés dans le futur.

Pour cela, nous pouvons passer l’ordre dans lequel nous voulons que les filtres soient appliqués à la méthode ofMany. De cette manière, nous classons les articles par published_at, et s’ils sont identiques, nous les classons par id. Deuxièmement, nous pouvons appliquer une fonction de requête à la méthode ofMany pour exclure tous les articles dont la publication est programmée !

<?php

public function currentPosts()
{
    return $this->hasOne(Post::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

Polymorphe plusieurs à plusieurs

Le many-to-many polymorphe est légèrement plus complexe que le normal. Une situation courante est celle où les balises s’appliquent à plusieurs actifs dans votre application. Par exemple, sur TikTok, nous avons des balises qui peuvent s’appliquer aux vidéos, aux courts métrages, aux histoires, etc.

La méthode polymorphe many-to-many nous permet d’avoir une seule table de balises associées aux vidéos, aux courts-métrages et aux histoires.

La structure du tableau est simple :

videos
    id – integer
    description – string

stories 
    id – integer
    description – string

taggables 
    tag_id – integer
    taggable_id – integer
    taggable_type – string

Les tables étant prêtes, nous pouvons créer le modèle et utiliser la méthode morphToMany. Cette méthode accepte le nom de la classe du modèle et le « nom de la relation » :

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Video extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

Grâce à cela, nous pouvons facilement définir la relation inverse. Nous savons que pour chaque modèle enfant, nous voulons appeler la méthode morphedByMany:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    public function stories()
    {
        return $this->morphedByMany(Story::class, 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    } 
}

Et maintenant, lorsque nous obtenons une balise, nous pouvons récupérer toutes les vidéos et les histoires liées à cette balise !

<?php
use App\Model\Tag;

$tag = Tag::find(10);
$posts = $tag->stories;
$videos = $tag->stories;

Optimisez Eloquent pour plus de rapidité

Lorsque vous travaillez avec l’ORM Eloquent de Laravel, il est essentiel de comprendre comment optimiser les requêtes de base de données et minimiser le temps et la mémoire nécessaires pour récupérer les données. L’une des façons d’y parvenir est d’implémenter la mise en cache dans votre application.

Laravel fournit un système de mise en cache flexible qui prend en charge différents backends, tels que Redis, Memcached et la mise en cache basée sur les fichiers. En mettant en cache les résultats des requêtes Eloquent, vous pouvez réduire le nombre de requêtes de base de données, ce qui rend votre application plus rapide et plus utile.

En outre, vous pouvez utiliser le constructeur de requêtes de Laravel pour créer des requêtes complexes supplémentaires, optimisant ainsi les performances de votre application.

En résumé

En conclusion, les relations Eloquent sont une fonctionnalité puissante de Laravel qui permet aux développeurs de travailler facilement avec des données liées. Qu’il s’agisse de relations un à un ou plusieurs à plusieurs, Eloquent fournit une syntaxe simple et intuitive pour définir et interroger ces relations.

En tant que développeur Laravel, la maîtrise des relations Eloquent peut considérablement améliorer votre flux de développement et rendre votre code plus efficace et plus lisible. Si vous souhaitez en savoir plus sur Laravel, Kinsta met à votre disposition diverses ressources, notamment un tutoriel sur la prise en main de Laravel et un article sur les salaires des développeurs Laravel.

Kinsta propose des solutions d’hébergement gérées qui facilitent le déploiement et la gestion des applications Laravel.

Coman Cosmin

Cosmin Coman is a technology writer and developer with over 3 years of experience. Apart from writing for Kinsta, he has assisted in research at nuclear physics facilities and universities. Tech-savvy and integrated into the community, he always comes up with innovative solutions.