Aller au contenu principal

Doctrine : relation ManyToMany

Dans nos entités, nous avons déjà créé une relation OneToMany entre Article et Category.

Nous avons vu que ce type de relation est utile pour intégrer automatiquement une clé étrangère dans la table Article. Dans le code, nous ne nous occupons pas des clés étrangères : nous établissons uniquement un lien entre deux classes, donc deux entités.

Rappel : Côté One, côté Many​

Quand on crée une relation, on ne peut pas la créer au hasard. Il faut respecter un sens.

Dans notre exemple, ce qu'il faut retenir, c'est qu'on détermine le sens en fonction de là où on part : si je pars de la catégorie, alors je peux dire qu'une (One) catégorie a plusieurs (Many) articles.

Ainsi, dans le maker, si je suis en train d'éditer l'entité Category, c'est bien une relation OneToMany que je dois indiquer. Si j'étais en train d'éditer l'entité Article, alors logiquement, je créerais une ManyToOne.

Clé étrangère​

Nous l'avons vu : dans les entités, donc les classes PHP, on ne s'occupe pas de la clé étrangère : c'est Doctrine qui s'en charge pour nous lors des migrations.

Dans le cas des relations OneToMany, ce qu'il faut retenir, c'est que c'est le côté Many qui détiendra la clé étrangère dans la base de données : en effet, si pour une catégorie donnée, on peut avoir N articles, alors ce sont les articles qui désigneront la catégorie à laquelle ils appartiennent, au travers d'une clé étrangère.

Dans le vocabulaire de Doctrine, on dira également que le côté Many (donc pour nous, Article) est le côté propriétaire de la relation.

Côtés : propriétaire/inversé​

Au sujet des relations Doctrine, il y a une notion importante à retenir : toute relation possède un côté propriétaire et un côté inversé.

En effet, quand on manipule un objet de type Category par exemple, on aimerait peut-être pouvoir récupérer ses articles avec getArticles(), mais quand on a un objet de type Article, on aimerait également récupérer sa catégorie avec getCategory().

Du point de vue de Doctrine, les objets contiennent donc des références à d'autres objets. On peut alors naviguer dans une relation pour récupérer des objets reliés, sans avoir à faire une requête SQL avec une jointure.

Mais, toujours du point de vue de Doctrine, il faut maintenir la synchronisation entre les deux côtés. Si je veux ajouter un article à une catégorie par exemple, dois-je faire $category->addArticle($article) ou l'inverse $article->setCategory($category) ?

Doctrine, pour assurer les mises à jour en base de données, ne vérifie les changements que du côté propriétaire de la relation : cela signifie qu'il faut utiliser $article->setCategory($category).

Mais est-ce que cela signifie qu'on ne peut pas utiliser $category->addArticle($article) ?

En réalité, le maker de Symfony a bien fait les choses. Dans l'entité Category, si on veut ajouter un article, alors il utilise en interne le setter du côté propriétaire :

src/Entity/Category.php
public function addArticle(Article $article): static
{
if (!$this->articles->contains($article)) {
$this->articles->add($article);
$article->setCategory($this);
}

return $this;
}

Ce mécanisme a été intégré automatiquement lorsqu'on a créé la relation avec le maker : on s'assure ainsi de la synchronisation des deux côtés de la relation.

En gros, tout va bien pour les relations OneToMany, le code gère la synchronisation. Mais qu'en est-il pour les relations N-N ?

Les relations ManyToMany​

Quand on conçoit une base de données avec un MCD, on peut se retrouver avec des cardinalités N-N. Par exemple, pour nos articles, si nous voulons ajouter un système de tags, d'étiquettes, alors on aimerait qu'un article possède potentiellement plusieurs tags, et que pour un tag, nous ayons plusieurs articles correspondants.

Avec Doctrine, on créera alors une relation ManyToMany.

Traditionnellement, quand on passe au MLD, une association N-N résulte en la création d'une table pivot, ou bien table de transition, contenant les couples de clés primaires de chaque côté de l'association. Pour nous, ce sont ces couples qui indiquent alors quels articles sont reliés à quels tags.

Doctrine gère automatiquement la création de cette table pivot. Quand on crée la relation ManyToMany entre deux entités, nous n'avons pas besoin de considérer la table de transition : la migration la prendra en charge automatiquement en SQL.

Côtés propriétaire et inversé​

Nous avons vu que dans le cas d'une relation OneToMany, c'est le côté Many qui est le propriétaire de la relation.

Le souci, c'est que dans une relation ManyToMany, nous avons deux côtés Many ! Comment déterminer le propriétaire ? 🤔

Pour cela, c'est nous qui devons réfléchir au fonctionnement que nous souhaitons implémenter, puis décider nous-mêmes du côté qui sera le propriétaire. En effet, chaque côté pourrait l'être ! Mais tout dépendra du fonctionnement voulu.

Par exemple, pour nos articles et nos tags, le fonctionnement que l'on souhaitera certainement est le suivant : depuis un formulaire de saisie d'article, on voudra probablement affecter plusieurs tags à cet article. On peut alors décider que le côté propriétaire est l'entité Article.

Pour créer la relation ManyToMany, on veillera alors à utiliser le maker pour éditer l'entité Article et non Tag. La relation sera configurée automatiquement pour désigner l'entité en cours d'édition comme le côté propriétaire de la relation.

Dans le code :

// Ici, "inversedBy" signifie bien "inversé par" : nous sommes donc du côté propriétaire
// et cette relation est inversée par l'autre côté : la propriété "articles" dans Tag
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'articles')]
private Collection $tags;
// Ici, nous sommes donc du côté inversé.
#[ORM\ManyToMany(targetEntity: Article::class, mappedBy: 'tags')]
private Collection $articles;

Lors de la migration, une table de transition est donc automatiquement créée entre Article et Tag.

Formulaire​

Une fois que notre relation ManyToMany a été créée et configurée correctement, on peut retourner dans le formulaire d'article, pour ajouter le champ concernant les tags :

src/Form/ArticleType.php
$builder
//...
->add('tags', EntityType::class, [
'class' => Tag::class,
'choice_label' => 'name',
'multiple' => true
])
//...
;
Multiple

Pour pouvoir sélectionner plusieurs tags, on notera la présence du paramètre multiple à true

attention

Si les côtés (propriétaire et inversé) étaient définis dans l'autre sens, alors on ne pourrait pas sauvegarder les associations entre des tags et un nouvel article

Résumé​

  • Une relation Doctrine possède un côté propriétaire et un côté inversé
  • Pour assurer les bonnes mises à jour en BDD, Doctrine ne vérifie que le côté propriétaire
  • Avec une relation OneToMany, le côté propriétaire est toujours le côté Many : celui qui détient la clé étrangère en BDD
  • Avec une relation ManyToMany, il appartient au développeur de déterminer le côté propriétaire
  • Quand on définit une relation ManyToMany dans le code, Doctrine gère tout seul la création d'une table pivot, ou de transition : pas besoin de la créer sous forme d'entité