Les relations avec Eloquent (1/2)

Eloquent est un ORM élégant et efficace. Son utilité essentielle se trouve dans le traitement de données relationnelles. Il est parfois délicat de le mettre en œuvre, surtout pour ceux qui ne sont pas vraiment habitués aux subtilités du modèle relationnel. Dans cet article je vais m’attacher à présenter les bases de ce domaine avec l’application d’Eloquent. Je vais faire un tour d’horizon complet. Alors c’est parti pour une visite guidée.

Dans cette première partie je vais présenter la construction des relations, je traiterai les problèmes de gestion des enregistrements liés dans un prochain article.

Nota : Cet article est la version Laravel 5 de celui-ci.

La base d’exemple

Pour que le voyage soit efficace on va avoir besoin d’une base de données pour tester les différents cas de figure. J’ai créé un modèle de base sur le site laravelsd.com :

img66

Ce site propose un outil simple et efficace pour créer des tables et un schéma relationnel de façon visuelle. La base que j’ai créée couvre pratiquement tous les cas possibles :

img40Ce n’est évidemment pas une situation très réaliste, son seul but est de donner l’occasion de présenter toutes les situations utiles pour cet article. L’outil génère gentiment les migrations, les modèles et même des vues ! Vous pouvez récupérer tout ça en cliquant simplement sur ce bouton :

img68

Alors avec une version neuve de Laravel 5, et après avoir créé et configuré une base, effectuez les migrations téléchargées et copiez les modèles. Comme il va nous falloir des enregistrements pour les tests j’ai créé ce seed :

<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;

class DatabaseSeeder extends Seeder {

	/**
	 * Run the database seeds.
	 *
	 * @return void
	 */
	public function run()
	{
		Model::unguard();
 
        for ($i = 1; $i < 11; $i++) {
            DB::table('categories')->insert(['name' => 'Category ' . $i]);
            DB::table('periods')->insert(['name' => 'Period ' . $i]);
            DB::table('countries')->insert(['name' => 'Country ' . $i]);
        }
 
        for ($i = 1; $i < 21; $i++) {
            DB::table('themes')->insert(['name' => 'Theme ' . $i]);
            DB::table('editors')->insert(['name' => 'Editor ' . $i]);
            DB::table('formats')->insert(['name' => 'Format ' . $i]);
            DB::table('contacts')->insert(['phone' => '00 00 00 00 00', 'editor_id' => $i]);
            DB::table('cities')->insert(['name' => 'City ' . $i, 'country_id' => rand(1, 10)]);
        }
 
        for ($i = 1; $i < 21; $i++) {
            DB::table('authors')->insert(['name' => 'Author ' . $i, 'city_id' => rand(1, 20)]);
        }
 
        for ($i = 1; $i < 51; $i++) {
            $choice = array('App\Models\Format','App\Models\Editor');
            DB::table('books')->insert([
                'name' => 'Book ' . $i,
                'bookable_id' => rand(1, 20),
                'theme_id' => rand(1, 20),
                'bookable_type' => $choice[rand(0,1)]
            ]);
        }

        for ($i = 1; $i < 21; $i++) {
            $number = rand(2, 8);
            $items = [];
            for ($j = 1; $j <= $number; $j++) {
                while(in_array($n = rand(1, 50), $items)) {}
                $items[] = $n;
                DB::table('author_book')->insert(array(
                    'book_id' => $n,
                    'author_id' => $i
                ));
            }
        }
 
        $items = [];
        for ($i = 1; $i < 81; $i++) {
            $choice = array('App\Models\Category','App\Models\Period');
            do{
                $t1 = rand(1, 20);
                $t2 = rand(1, 20);
                $t3 = $choice[rand(0,1)];
            } while(in_array($t1 . $t2 . $t3, $items));
            $items[] = $t1 . $t2 . $t3;
            DB::table('themables')->insert([
                'theme_id' => $t1,
                'themable_id' => $t2,
                'themable_type' => $t3
            ]);
        }
	}

}

C’est un remplissage artificiel mais efficace. Maintenant que l’intendance est en place voyons un peu tout ça en détail…

La relation 1:1

Cette relation est la plus simple mais la moins utile. De quoi s’agit-il ? Prenons le cas de notre exemple :

img41J’ai une table editors et une table contacts. Un éditeur a un numéro de téléphone, celui-ci se trouve enregistré dans la table contact. Comment est établie la relation ? On trouve dans la table contacts le champ editor_id qui correspond à l’id de l’éditeur. Je l’ai surligné pour mieux le visualiser. C’est comme cela qu’on peut savoir qu’un numéro de téléphone dans la table contacts correspond à un éditeur dans la table editors. On appelle ce champ clé étrangère parce que c’est une valeur qui appartient à une autre table et qui est juste insérée ici pour la relation.

Il ne faut pas analyser longtemps cette situation pour se rendre compte qu’on aurait tout aussi bien pu prévoir le champ phone dans la table editors ! C’est toujours le cas avec une relation de type 1:1 mais alors pourquoi créer deux tables ? En général c’est pour des raison de performances si on a des données volumineuses pas souvent accédées, il devient alors intéressant de les mettre dans une table séparée. Il peut aussi s’avérer judicieux quelques rares fois de répartir les données. Autrement dit vous ne rencontrerez pas souvent cette situation ! Mais son intérêt est qu’elle est facile à comprendre et constitue une bonne introduction aux suivantes.

Pour comprendre une relation il faut se positionner dans une table (en pensée) et se poser les bonnes questions. On a donc deux cas.

hasOne

Je suis dans la table editors. Je regarde la table contacts et je me dis « j’ai là un numéro de téléphone ». En langage Eloquent on traduit ça par hasOne :

img42Pour traduire cela de façon concrète on crée dans le modèle Editor une méthode :

public function contact()
{
	return $this->hasOne('App\Models\Contact');
}

Pour établir la relation Eloquent part du principe que la clé étrangère est construite à partir du nom du modèle auquel on adjoint « _id », ce qui donne bien editor_id. On a d’autres possibilités, regardez la méthode dans la classe Model :

public function hasOne($related, $foreignKey = null, $localKey = null)
{
	$foreignKey = $foreignKey ?: $this->getForeignKey();

	$instance = new $related;

	$localKey = $localKey ?: $this->getKeyName();

	return new HasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}

Le deuxième paramètre permet de renseigner la clé étrangère si elle ne respecte pas la norme, donc on peut écrire par exemple :

public function contact()
{
	return $this->hasOne('App\Models\Contact', 'editor');
}

Cela pour le cas où la clé étrangère se nomme « editor ». Vous remarquez aussi qu’on dispose d’un troisième paramètre, il sert à renseigner le nom de l’id de la table d’origine si celui-ci ne se nomme pas id. Le cas peut se présenter si vous utilisez Eloquent sur une table existante et que vous ne désirez pas tout changer. Je peux maintenant écrire cela :

$telephone = \App\Models\Editor::first()->contact()->first()->phone;

Et je trouve le numéro de téléphone de l’éditeur sélectionné. Eloquent génère ces deux requêtes :

select * from `editors` limit 1 (440μs)
select * from `contacts` where `contacts`.`editor_id` = '1' limit 1 (530μs)

Avec le query builder ce serait plus laborieux pour obtenir le même résultat :

$editeur = \DB::table('editors')->first();
$contact = \DB::table('contacts')->where('editor_id', $editeur->id)->first();
$telephone =  $contact->phone;

Comme Eloquent est très conciliant vous pouvez même écrire cela :

$telephone = Editor::first()->contact->phone;

Ce qui rend la syntaxe particulièrement élégante et lisible !

belongsTo

Envisageons maintenant l’inverse. Cette fois je me place dans la table des contacts. Là je me dis « j’ai un numéro de téléphone qui appartient à un éditeur ». En langage Eloquent on traduit ça par belongsTo :

img43Pour traduire cela de façon concrète on crée dans le modèle Contact une méthode :

public function editor()
{
    return $this->belongsTo('App\Models\Editor');
}

Attention ! Pour établir la relation Eloquent cette fois utilise le nom de la méthode ! La clé étrangère est construite à partir du nom de la méthode auquel on adjoint « _id », ce qui donne bien editor_id dans notre cas. On a d’autres possibilités, regardez la méthode dans la classe Model :

public function belongsTo($related, $foreignKey = null, $otherKey = null, $relation = null)
{
	// If no relation name was given, we will use this debug backtrace to extract
	// the calling method's name and use that as the relationship name as most
	// of the time this will be what we desire to use for the relatinoships.
	if (is_null($relation))
	{
		list(, $caller) = debug_backtrace(false);

		$relation = $caller['function'];
	}

	// If no foreign key was supplied, we can use a backtrace to guess the proper
	// foreign key name by using the name of the relationship function, which
	// when combined with an "_id" should conventionally match the columns.
	if (is_null($foreignKey))
	{
		$foreignKey = snake_case($relation).'_id';
	}

	$instance = new $related;

	// Once we have the foreign key names, we'll just create a new Eloquent query
	// for the related models and returns the relationship instance which will
	// actually be responsible for retrieving and hydrating every relations.
	$query = $instance->newQuery();

	$otherKey = $otherKey ?: $instance->getKeyName();

	return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation);
}

On peut donc renseigner le nom de la clé étrangère si elle ne correspond pas à la norme :

public function editor()
{
    return $this->belongsTo('App\Models\Editor', 'editor');
}

Vous pouvez aussi avec le troisième paramètre indiquer le nom de la clé de la table editors si elle ne s’appelle pas « id ».

On peut maintenant écrire cela :

$nom = \App\models\Contact::find(5)->editor()->first()->name;

Et on trouve le nom de l’éditeur pour le contact sélectionné. Eloquent génère ces deux requêtes :

select * from `contacts` where `contacts`.`id` = '5' limit 1 (440μs)
select * from `editors` where `editors`.`id` = '5' limit 1 (510μs)

Comme Eloquent est très conciliant vous pouvez même écrire cela :

$nom = \App\models\Contact::find(5)->editor->name;

Ce qui rend la syntaxe encore particulièrement élégante et lisible !

La relation 1:n

Cette relation est de loin la plus courante ! Dans la base d’exemple on trouve cette relation par exemple entre les pays et les villes :

img44J’ai une table countries et une table cities. Un pays peut avoir plusieurs villes, par contre une ville est forcément située dans un seul pays. Ce type de relation est dissymétrique, contrairement à la relation 1:1 que nous avons vu ci-dessus.

Comment est établie la relation ? On trouve dans la table cities le champ country_id qui correspond à l’id du pays. Je l’ai surligné pour mieux le visualiser. C’est comme cela qu’on peut savoir qu’une ville correspond à un certain pays. On appelle ce champ clé étrangère parce que c’est une valeur qui appartient à une autre table et qui est juste insérée ici pour la relation.

On va à nouveau envisager la relation en se positionnant dans chacune des tables.

hasMany

Commençons par nous situer dans la table des pays. Je me dis « j’ai beaucoup de villes » :

img45Pour traduire cela de façon concrète on crée dans le modèle Country une méthode :

public function cities()
{
    return $this->hasMany('App\Models\City');
}

Voyons cette méthode dans la classe Model :

public function hasMany($related, $foreignKey = null, $localKey = null)
{
	$foreignKey = $foreignKey ?: $this->getForeignKey();

	$instance = new $related;

	$localKey = $localKey ?: $this->getKeyName();

	return new HasMany($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}

Si la clé étrangère n’est pas renseignée elle est construite à partir du nom du modèle. Dans notre cas on a donc « country_id ». Vous avez aussi la possibilité de renseigner le deuxième paramètre si votre champ ne correspond pas à la norme :

public function cities()
{
    return $this->hasMany('App\Models\City', 'country');
}

Le troisième paramètre sert en indiquer le nom de la clé pour la table des pays si ce n’est pas « id », comme on l’a déjà vu pour les autre méthodes ci-dessus.

On peut maintenant écrire cela :

\App\Models\Country::find(4)->cities->each(function($city)
{
    echo $city->name, '<br>';
});

Je trouve toutes les villes pour le pays d’id 4. J’itère dans la collection obtenue pour récupérer les noms des villes. Eloquent génère ces 2 requêtes :

select * from `countries` where `countries`.`id` = '4' limit 1 (470μs)
select * from `cities` where `cities`.`country_id` = '4' (320μs)

Dans mon cas j’obtiens ce résultat avec une seule ville :

City 8

Vous pouvez évidemment avoir un résultat différent étant donné que j’ai rempli les tables avec des valeurs aléatoires.

belongsTo

Voyons maintenant la relation à partir de la table des villes :

img46Là je me dis « j’appartiens à un seul pays ». On retrouve donc le belongsTo qu’on à déjà vu pour la relation de type 1:1. La situation est strictement la même et donc le traitement identique. Tout ce que j’ai dit ci-dessus est donc valable aussi pour la relation de type 1:n. En fait Eloquent est totalement ignorant du type de relation, son seul repère est la méthode utilisée. Donc quand vous employez la méthode belongsTo il ne sait absolument pas si vous avez mis en place une relation 1:1 ou 1:n et il n’en a pas besoin. Il agit de façon localisée.

hasManyThrough

Voyons maintenant une extension de hasMany avec hasManyThrough. Regardez cette partie de la base d’exemple :

img47On a la relation 1:n vue ci-dessus entre les pays et les villes. On a le même type de relation entres les villes et les auteurs. La méthode hasManyThrough permet de « traverser » la table des villes pour mettre directement en relation la table des pays avec celle des auteurs :

img48Autrement dit on veut tous les auteurs d’un pays. Pour traduire cela de façon concrète on crée dans le modèle Country une méthode authors :

public function authors()
{
	return $this->hasManyThrough('App\Models\Author', 'App\Models\City');
}

Voyons cette méthode dans la classe Model :

public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null)
{
	$through = new $through;

	$firstKey = $firstKey ?: $this->getForeignKey();

	$secondKey = $secondKey ?: $through->getForeignKey();

	return new HasManyThrough(with(new $related)->newQuery(), $this, $through, $firstKey, $secondKey);
}

Analysons les paramètres :

  1. C‘est le modèle de destination, dans notre cas Author
  2. C’est le modèle intermédiaire, dans notre cas City
  3. Là on peut mettre la clé étrangère dans la table intermédiaire si elle n’est pas à la norme, nous on a bien country_id
  4. Là on peut mettre la clé étrangère dans la table finale si elle n’est pas à la norme, nous on a bien city_id

Maintenant on peut écrire ça :

\App\Models\Country::find(10)->authors->each(function($author)
{
    echo $author->name, '<br>';
});

J’obtiens ce résultat dans mon cas :

Auteur 2
Auteur 6

Eloquent s’en sort avec deux requêtes dont une jointure :

select * from `countries` where `countries`.`id` = '10' limit 1 (430μs)
select `authors`.*, `cities`.`country_id` from `authors` inner join `cities` on `cities`.`id` = `authors`.`city_id` where `cities`.`country_id` = '10' (390μs)

Maintenant comment faire l’inverse de hasManyThrough ? Pas besoin d’inventer quelque chose de nouveau, il suffit d’enchaîner deux belongsTo. Par exemple ce code :

\App\Models\Author::find(2)->city->country->name;

Va nous donner le nom du pays pour l’auteur d’id 2. Dans mon cas je trouve évidemment Country 10. Elégant non ?

La relation n:n

La relation n:n est la plus complexe des 3. Voyons un exemple dans la base pour comprendre de quoi il s’agit :

img49On a une table des auteurs et une table des livres. Un auteur peut écrire plusieurs livres et réciproquement un livre peut être écrit par plusieurs auteurs. Il est impossible de relier directement les deux tables avec des clés étrangères pour régler cette situation. On est donc obligé de créer une table intermédiaire appelée pivot chargée de mémoriser les deux clés étrangères et d’effectuer ainsi la liaison entre les deux tables. Ici c’est la table author_book.

Par convention la table pivot doit comporter le nom des deux modèles séparés par un underscore. Par convention également on met respecte l’ordre alphabétique, on a donc en premier author. On trouve dans la table pivot les deux clés étrangères :

  1. la clé author_id qui référence un enregistrement de la table des auteurs
  2. la clé book_id qui référence un enregistrement de la table des livres

belongsToMany

Comme la relation est symétrique on a une seule méthode : belongsToMany. En effet si je me place du côté de la table des auteurs je me dis « j’appartiens à plusieurs livres » (bon c’est une image, il ne faut pas que les auteurs se sentent possédés par leurs œuvres !) et réciproquement du côté de la table des livres je me dis « j’appartiens à plusieurs auteurs » :

img50Pour concrétiser ça on crée une méthode books dans le modèle Author :

public function books()
{
	return $this->belongsToMany('App\Models\Book');
}

Et évidemment la réciproque authors dans le modèle Book :

public function authors()
{
	return $this->belongsToMany('App\Models\Author');
}

Regardez la signature de la méthode belongsToMany :

public function belongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null)

Les paramètres doivent vous être maintenant familiers parce qu’ils se répètent au fil des méthodes :

  1. là on met le nom du modèle de la table visée
  2. là on met le nom de la table pivot s’il n’est pas conventionnel, nous on a bien choisi author_book
  3. là on met le nom de la clé étrangère de la table de départ s’il n’est pas conventionnel
  4. là on met le nom de la clé étrangère de la table d’arrivée s’il n’est pas conventionnel

Maintenant si on veut connaître tous les titres des livres d’un auteur c’est facile :

\App\Models\Author::find(4)->books->each(function($book)
{
    echo $book->name, '<br>';
});

Dans mon cas j’obtiens :

Book 48
Book 41
Book 12

Voici les requêtes générées :

select * from `authors` where `authors`.`id` = '4' limit 1 (580μs)
select `books`.*, `author_book`.`author_id` as `pivot_author_id`, `author_book`.`book_id` as `pivot_book_id` from `books` inner join `author_book` on `books`.`id` = `author_book`.`book_id` where `author_book`.`author_id` = '4' (450μs)

De la même manière on peut récupérer le nom des auteurs d’un livre :

\App\Models\Book::find(4)->authors->each(function($author)
{
    echo $author->name, '<br>';
});

Ici j’obtiens :

Auteur 7
Auteur 12

Relation polymorphique de type 1:n

Maintenant que vous êtes chaud on peut aborder un sujet un peu plus délicat. Regardez cette partie de la base d’exemple :

img51On retrouve notre table books déjà vue pour une autre relation. On a aussi deux autres tables : editors et formats. Voici ce que l’on veut :

  1. un éditeur peut correspondre à plusieurs livres mais un livre ne peut appartenir qu’à un éditeur
  2. un format peut correspondre à plusieurs livres mais un livre ne peut appartenir qu’à un format
  3. un livre appartient soit à un éditeur, soit à un format, mais pas aux deux.

On se rend compte qu’on aurait du mal pour s’en sortir avec deux relations 1:n comme on l’a fait précédemment. On a deux relations de même type qui s’adressent à la même table. Ces deux relations on la même structure mais changent dans leur forme, d’où l’appelation de « polymorphique » qui veut tout simplement dire « plusieurs formes ».

Dans une relation classique 1:n on établit le lien entre les tables avec une clé étrangère dans la table du côté n et ça suffit pour s’y retrouver sans ambiguïté. Mais là comment va-t-on faire ? Il nous faut deux renseignements :

  1. le nom de la table en relation ou de son modèle
  2. l’id de l’enregistrement concerné

Regardez dans la table des livres, vous trouvez deux champs :

  1. bookable_id qui est chargé de contenir l’id de l’enregistrement lié
  2. bookable_type qui est chargé de contenir le nom du modèle de la table en relation

Si on sait quelle est la table reliée et qu’on connait aussi l’id de l’enregistrement alors la relation est parfaitement connue. Voici une visualisation de la relation :

img52morphTo

Eloquent propose la méthode morphTo pour établir une relation polymorphique à partir de la table qui est du côté n. Il faut donc prévoir cette méthode dans le modèle Book :

public function bookable()
{
	return $this->morphTo();
}

Voici la signature de la méthode morphTo :

public function morphTo($name = null, $type = null, $id = null)

Voyons les paramètres :

  1. c’est le nom de la relation, si on en donne pas c’est le nom de la méthode qui est utilisé
  2. c’est le nom du champ qui récupère le type du modèle en liaison, si on en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _type », dans notre cas on a déjà bookable_type
  3. c’est le nom du champ qui récupère la clé étrangère, si on en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _id », dans notre cas on a déjà bookable_id

Bon maintenant vous vous demandez peut-être pourquoi se donner autant de peine ? Regardez ce code :

\App\Models\Book::take(5)->get()->each(function($book)
{
    echo $book->bookable->name, '<br>';
});

Je prends les 5 premiers livres et dans une boucle je récupère le nom dans la table en relation. La méthode bookable va créer une instance du modèle correspondant, soit Editor, soit Format selon que le livre est en relation avec l’un ou l’autre. Voilà ce que j’obtiens :

Editor 15
Format 18
Format 17
Editor 16
Editor 11

Avec ces requêtes :

select * from `books` limit 5 (460μs)
select * from `editors` where `editors`.`id` = '15' limit 1 (320μs)
select * from `formats` where `formats`.`id` = '18' limit 1 (300μs)
select * from `formats` where `formats`.`id` = '17' limit 1 (310μs)
select * from `editors` where `editors`.`id` = '16' limit 1 (290μs)
select * from `editors` where `editors`.`id` = '11' limit 1 (260μs)

morphMany

Voyons à présent la relation inverse avec morphMany. Par exemple pour les éditeurs on doit ajouter la méthode dans le modèle :

public function books()
{
	return $this->morphMany('App\Models\Book', 'bookable');
}

Voici la signature de cette méthode :

public function morphMany($related, $name, $type = null, $id = null, $localKey = null)

Voyons les paramètres :

  1. c’est le nom du modèle de la table ciblée, dans notre cas Book
  2. c’est le nom de la relation, dans notre cas bookable
  3. c’est le nom du champ qui récupère le type du modèle en liaison, si on en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _type », dans notre cas on a déjà bookable_type
  4. c’est le nom du champ qui récupère la clé étrangère, si on en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _id », dans notre cas on a déjà bookable_id
  5. c’est le nom de l’id de la table de départ, dans notre cas on a « id », valeur par défaut

Pour le fonctionnement c’est exactement comme un hasMany. On peut par exemple trouver les livres d’un éditeur avec ce code :

\App\Models\Editor::find(6)->books->each(function($book)
{
    echo $book->name, '<br>';
});

Dans mon cas j’obtiens ça :

Book 6
Book 43

Avec ces requêtes :

select * from `editors` where `editors`.`id` = '6' limit 1 (490μs)
select * from `books` where `books`.`bookable_id` = '6' and `books`.`bookable_type` = 'App\Models\Editor' (420μs)

Relation polymorphique de type n:n

Voyons à présent la situation la plus complexe, qui est apparue avec la version 4.1 de Laravel, avec les relations polymorphiques de type n:n. Ce que nous avons vu précédemment devrait vous aider à comprendre de quoi il s’agit. Regardez cette partie de la base d’exemple :

img53

On a une table themes et deux tables : categories et periods. On veut relier la table des thèmes aux deux autres tables avec ces possibilités :

  1. une catégorie peut avoir plusieurs thèmes
  2. une période peut avoir plusieurs thèmes
  3. un thème peut avoir plusieurs catégories et plusieurs thèmes

On est donc dans le cadre d’une relation de type n:n. On a vu précédemment qu’on résout ce genre de situation avec une table pivot. Ici cette table pivot est themables. Dans une relation classique n:n on a vu que cette table pivot contient les clés des deux tables en relation. Ici nous n’avons pas deux tables mais trois. On peut évidemment s’en sortir en créant 2 tables pivots. La solution polymorphique est plus élégante. Il y a eu une discussion intéressante sur le sujet lors de sa soumission initiale. Voici comment cela est mis en oeuvre :

  • du côté de la table des thèmes on a pas de souci parce qu’on a une seule table, donc on peut mettre dans la table pivot la clé theme_id,
  • du côté des tables categories et periodes on adopte ce qu’on a vu pour la relation polymorphique précédente avec deux champs : themable_id pour la clé soit d’une catégorie, soit d’une période, et themable_type pour le nom du modèle de la table en liaison : categories ou periods.

Voici une visualisation de ce que nous allons mettre en place :

img54

morphToMany

De côté des deux tables on utilise la méthode morphToMany. Dans le modèle Category :

public function themes()
{
	return $this->morphToMany('App\Models\Theme', 'themable');
}

Et dans le modèle Period :

public function themes()
{
	return $this->morphToMany('App\Models\Theme', 'themable');
}

Voici la signature de la méthode morphToMany :

public function morphToMany($related, $name, $table = null, $foreignKey = null, $otherKey = null, $inverse = false)

Pas moins que 6 paramètres :

  1. c’est le nom du modèle de la table ciblée, dans notre cas Theme
  2. c’est le nom de la relation, dans notre cas themable
  3. c’est le nom de la table pivot construit à partir du deuxième paramètre auquel on ajoute « s », dans notre cas on a déjà themables
  4. c’est le nom du champ qui récupère la clé étrangère, si on en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _id », dans notre cas on a déjà themable_id
  5. c’est le nom du champ qui récupère le type du modèle en liaison, si on en donne pas il est nommé à partir du nom de la relation auquel on ajoute « _type », dans notre cas on a déjà themable_type
  6. ce dernier paramètre m’intrigue, je ne l’ai pas encore vraiment compris donc il méritera des tests complémentaires…

On peut maintenant trouver tous les thèmes pour une catégorie :

\App\Models\Category::find(2)->themes->each(function($theme)
{
    echo $theme->name, '<br>';
});

Ou pour une période :

\App\Models\Period::find(2)->themes->each(function($theme)
{
    echo $theme->name, '<br>';
});

Avec ces requêtes :

elect * from `periods` where `periods`.`id` = '2' limit 1 (420μs)
select `themes`.*, `themables`.`themable_id` as `pivot_themable_id`, `themables`.`theme_id` as `pivot_theme_id` from `themes` inner join `themables` on `themes`.`id` = `themables`.`theme_id` where `themables`.`themable_id` = '2' and `themables`.`themable_type` = 'App\Models\Period' (660μs)

morphedByMany

Voyons maintenant la relation inverse morphedByMany. On se place cette fois du côté de la table des thèmes. On doit déclarer une méthode par table reliée :

public function categories()
{
	return $this->morphedByMany('App\Models\Category', 'themable');
}

public function periods()
{
	return $this->morphedByMany('App\Models\Period', 'themable');
}

La méthode morphedByMany a cette signature :

public function morphedByMany($related, $name, $table = null, $foreignKey = null, $otherKey = null)

Je ne commente pas ces paramètres qui sont les mêmes que ceux de la méthode morphToMany. On peut alors trouver par exemple toutes la catégories pour un thème :

\App\Models\Theme::find(2)->categories->each(function($category)
{
    echo $category->name, '<br>';
});

Avec ces requêtes :

select * from `themes` where `themes`.`id` = '2' limit 1 (540μs)
select `categories`.*, `themables`.`theme_id` as `pivot_theme_id`, `themables`.`themable_id` as `pivot_themable_id` from `categories` inner join `themables` on `categories`.`id` = `themables`.`themable_id` where `themables`.`theme_id` = '2' and `themables`.`themable_type` = 'App\Models\Category' (440μs)

Avec cette relation polymorphique s’achève ce tour d’horizon des relations traitées par Eloquent. Il existe aussi la méthode morphOne pour les relations de type 1:1 mais franchement c’est anecdotique étant donné la rareté de la chose, elle fonctionne comme morphMany, elle n’est d’ailleurs même pas citée dans la documentation.

J’aborderai dans un prochain article la gestion de tout ça. En effet quand on crée des relations ça a évidemment une conséquence sur la manipulation des enregistrements dans les tables et ce n’est pas toujours rose.

Une réflexion sur “Les relations avec Eloquent (1/2)

Laisser un commentaire