Maintenant que vous savez créer et manipuler une entité simple, on va monter en puissance.
L'objectif de ce chapitre est de construire un ensemble d'entités en relation les unes avec les autres. Ces relations permettent de disposer d'un ensemble cohérent, qui se manipule simplement et en toute sécurité pour votre base de données.
- Présentation
- Relation One-To-One
- Relation Many-To-One
- Relation Many-To-Many
- Relation Many-To-Many avec attributs
- Les relations bidirectionnelles
Présentation
Présentation
Vous savez déjà stocker vos entités indépendamment les unes des autres, c'est très bien. Simplement, on est rapidement limités ! L'objectif de ce chapitre est de vous apprendre à établir des relations entre les entités.
Rappelez-vous, au début de la partie sur Doctrine2, je vous avais promis des choses comme <?php $article->getCommentaires()
. Eh bien, c'est cela que nous allons faire ici !
Les différents types de relations
Il y a plusieurs façons de lier des entités entre elles. En effet, il n'est pas pareil de lier une multitude de commentaires à un seul article et de lier un membre à un seul groupe. Il existe donc plusieurs types de relations, pour répondre à plusieurs besoins concrets. Ce sont les relations OneToOne
, OneToMany
et ManyToMany
. On les étudie juste après ces quelques notions de base à avoir.
Notions techniques d'ORM à savoir
Avant de voir en détail les relations, il faut comprendre comment elles fonctionnent. N'ayez pas peur, il y a juste deux notions à savoir avant d'attaquer.
Notion de propriétaire et d'inverse
La notion de propriétaire et d'inverse est abstraite mais importante à comprendre. Dans une relation entre deux entités, il y a toujours une entité dite propriétaire, et une dite inverse. Pour comprendre cette notion, il faut revenir à la vieille époque, lorsque l'on faisait nos bases de données à la main. L'entité propriétaire est celle qui contient la référence à l'autre entité. Attention, cette notion — à avoir en tête lors de la création des entités — n'est pas liée à votre logique métier, elle est purement technique.
Prenons un exemple simple, toujours les commentaires d'un article de blog. Vous disposez de la table commentaire
et de la table article
. Pour créer une relation entre ces deux tables, vous allez mettre naturellement une colonne article_id
dans la table commentaire
. La table commentaire
est donc propriétaire de la relation, car c'est elle qui contient la colonne de liaison article_id
. Assez simple au final !
N'allez pas me créer une colonne article_id
dans la table des commentaires ! C'est une image, de ce que vous faisiez avant. Aujourd'hui on va laisser Doctrine gérer tout ça, et on ne va jamais mettre la main dans PhpMyAdmin. Rappelez-vous : on pense objet dorénavant, et pas base de données.
Notion d'unidirectionnalité et de bidirectionnalité
Cette notion est également simple à comprendre : une relation peut être à sens unique ou à double sens. On ne va traiter dans ce chapitre que les relations à sens unique, dites unidirectionnelles. Cela signifie que vous pourrez faire <?php $entiteProprietaire->getEntiteInverse()
(dans notre exemple <?php $commentaire->getArticle()
), mais vous ne pourrez pas faire <?php $entiteInverse->getEntiteProprietaire()
(pour nous, <?php $article->getCommentaires()
). Attention, cela ne nous empêchera pas de récupérer les commentaires d'un article, on utilisera juste une autre méthode, via l'EntityRepository.
Cette limitation nous permet de simplifier la façon de définir les relations. Pour bien travailler avec, il suffit juste de se rappeler qu'on ne peut pas faire $entiteInverse->getEntiteProprietaire()
.
Pour des cas spécifiques, ou des préférences dans votre code, cette limitation peut être contournée en utilisant les relations à double sens, dites bidirectionnelles. Je les expliquerai rapidement à la fin de ce chapitre.
Rien n'est magique
Non, rien n'est magique. Je dois vous avertir qu'un <?php $article->getCommentaires()
est vraiment sympa, mais qu'il déclenche bien sûr une requête SQL ! Lorsqu'on récupère une entité (notre $article
par exemple), Doctrine ne récupère pas toutes les entités qui lui sont liées (les commentaires dans l'exemple), et heureusement ! S'il le faisait, cela serait extrêmement lourd. Imaginez qu'on veuille juste récupérer un article pour avoir son titre, et Doctrine nous récupère la liste des 54 commentaires, qui en plus sont liés à leurs 54 auteurs respectifs, etc. !
Doctrine utilise ce qu'on appelle le Lazy Loading, « chargement fainéant » en français. C'est-à-dire qu'il ne va charger les entités à l'autre bout de la relation que si vous voulez accéder à ces entités. C'est donc pile au moment où vous faites <?php $article->getCommentaires()
que Doctrine va charger les commentaires (avec une nouvelle requête SQL donc) puis va vous les transmettre.
Heureusement pour nous, il est possible d'éviter cela ! Parce que cette syntaxe est vraiment pratique, il serait dommage de s'en priver pour cause de requêtes SQL trop nombreuses. Il faudra simplement utiliser nos propres méthodes pour charger les entités, dans lesquelles nous ferons des jointures toutes simples. L'idée est de dire à Doctrine : « Charge l'entité Article
, mais également tous ses commentaires ». Avoir nos propres méthodes pour cela permet de ne les exécuter que si nous voulons vraiment avoir les commentaires en plus de l'article. En somme, on se garde le choix de charger ou non la relation.
Mais nous verrons tout cela dans le prochain chapitre sur les repositories. Pour l'instant, revenons à nos relations !
Relation One-To-One
Présentation
La relation One-To-One, ou 1..1, est assez classique. Elle correspond, comme son nom l'indique, à une relation unique entre deux objets.
Pour illustrer cette relation dans le cadre du blog, nous allons créer une entité Image
. Imaginons qu'on offre la possibilité de lier une image à un article, une sorte d'icône pour illustrer un peu l'article. Si à chaque article on ne peut afficher qu'une seule image, et que chaque image ne peut être liée qu'à un seul article, alors on est bien dans le cadre d'une relation One-To-One. La figure suivante schématise tout cela.
Tout d'abord, histoire qu'on parle bien de la même chose, créez cette entité Image
avec au moins les attributs url
et alt
pour qu'on puisse l'afficher correctement. Voici la mienne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <?php // src/Sdz/BlogBundle/Entity/Image.php namespace Sdz\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Sdz\BlogBundle\Entity\Image * * @ORM\Table() * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ImageRepository") */ class Image { /** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string $url * * @ORM\Column(name="url", type="string", length=255) */ private $url; /** * @var string $alt * * @ORM\Column(name="alt", type="string", length=255) */ private $alt; // Les getters et setters } |
Définition de la relation dans les entités
Annotation
Pour établir une relation One-To-One entre deux entités Article
et Image
, la syntaxe est la suivante :
Entité propriétaire, Article
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php // src/Sdz/BlogBundle/Entity/Article.php /** * @ORM\Entity */ class Article { /** * @ORM\OneToOne(targetEntity="Sdz\BlogBundle\Entity\Image", cascade={"persist"}) */ private $image; // … } |
Entité inverse, Image
:
1 2 3 4 5 6 7 8 9 10 11 12 | <?php // src/Sdz/BlogBundle/Entity/Image.php /** * @ORM\Entity */ class Image { // Nul besoin d'ajouter une propriété ici // … } |
La définition de la relation est plutôt simple, mais détaillons-la bien.
Tout d'abord, j'ai choisi de définir l'entité Article
comme entité propriétaire de la relation, car un Article
« possède » une Image
. On aura donc plus tendance à récupérer l'image à partir de l'article que l'inverse. Cela permet également de rendre indépendante l'entité Image
: elle pourra être utilisée par d'autres entités que Article
, de façon totalement invisible pour elle.
Ensuite, vous voyez que seule l'entité propriétaire a été modifiée, ici Article
. C'est parce qu'on a une relation unidirectionnelle, rappelez-vous, on peut donc faire $article->getImage()
, mais pas $image->getArticle()
. Dans une relation unidirectionnelle, l'entité inverse, ici Image
, ne sait en fait même pas qu'elle est liée à une autre entité, ce n'est pas son rôle.
Enfin, concernant l'annotation en elle-même :
1 | @ORM\OneToOne(targetEntity="Sdz\BlogBundle\Entity\Image", cascade={"persist"}) |
Il y a plusieurs choses à savoir sur cette annotation :
- Elle est incompatible avec l'annotation
@ORM\Column
qu'on a vue dans un chapitre précédent. En effet, l'annotationColumn
définit une valeur (un nombre, une chaine de caractères, etc.), alors queOneToOne
définit une relation vers une autre entité. Lorsque vous utilisez l'un, vous ne pouvez pas utiliser l'autre sur le même attribut. - Elle possède au moins l'option
targetEntity
, qui vaut simplement le namespace complet vers l'entité liée. - Elle possède d'autres options facultatives, dont l'option
cascade
dont on parle un peu plus loin.
N'oubliez pas de mettre à jour la base de données avec la commande doctrine:schema:update
!
Rendre une relation facultative
Par défaut, une relation est facultative, c'est-à-dire que vous pouvez avoir un Article
qui n'a pas d'Image
liée. C'est le comportement que nous voulons pour l'exemple : on se donne le droit d'ajouter un article sans forcément trouver une image d'illustration. Si vous souhaitez forcer la relation, il faut ajouter l'annotation JoinColumn
et définir son option nullable
à false
, comme ceci :
1 2 3 4 5 | /** * @ORM\OneToOne(targetEntity="Sdz\BlogBundle\Entity\Image") * @ORM\JoinColumn(nullable=false) */ private $image; |
Les opérations de cascade
Parlons maintenant de l'option cascade
que l'on a vu un peu plus haut. Cette option permet de « cascader » les opérations que l'on ferait sur l'entité Article
à l'entité Image
liée par la relation.
Pour prendre l'exemple le plus simple, imaginez que vous supprimiez une entité Article
via un <?php $em->remove($article)
. Si vous ne précisez rien, Doctrine va supprimer l'Article
mais garder l'entité Image
liée. Or ce n'est pas forcément ce que vous voulez ! Si vos images ne sont liées qu'à des articles, alors la suppression de l'article doit entraîner la suppression de l'image, sinon vous aurez des Images
orphelines dans votre base de données. C'est le but de cascade
. Attention, si vos images sont liées à des articles mais aussi à d'autres entités, alors vous ne voulez pas forcément supprimer directement l'image d'un article, car elle pourrait être liée à une autre entité.
On peut cascader des opérations de suppression, mais également de persistance. En effet, on a vu qu'il fallait persister une entité avant d'exécuter le flush()
, afin de dire à Doctrine qu'il doit enregistrer l'entité en base de données. Cependant, dans le cas d'entités liées, si on fait un $em->persist($article)
, qu'est-ce que Doctrine doit faire pour l'entité Image
contenue dans l'entité Article
? Il ne le sait pas et c'est pourquoi il faut le lui dire : soit en faisant manuellement un persist()
sur l'article et l'image, soit en définissant dans l'annotation de la relation qu'un persist()
sur Article
doit se « propager » sur l'Image
liée.
C'est ce que nous avons fait dans l'annotation : on a défini le cascade
sur l'opération persist()
, mais pas sur l'opération remove()
(car on se réserve la possibilité d'utiliser les images pour autre chose que des articles).
Getter et setter
D'abord, n'oubliez pas de définir un getter et un setter dans l'entité propriétaire, ici Article
. Vous pouvez utiliser la commande php app/console doctrine:generate:entities SdzBlogBundle:Article
, ou alors prendre ce code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <?php // src/Sdz/BlogBundle/Entity/Article.php /** * @ORM\Entity */ class Article { /** * @ORM\OneToOne(targetEntity="Sdz\BlogBundle\Entity\Image") */ private $image; // Vos autres attributs… /** * @param Sdz\BlogBundle\Entity\Image $image */ public function setImage(\Sdz\BlogBundle\Entity\Image $image = null) { $this->image = $image; } /** * @return Sdz\BlogBundle\Entity\Image */ public function getImage() { return $this->image; } // Vos autres getters/setters… } |
Vous voyez qu'on a forcé le type de l'argument pour le setter setImage()
: cela permet de déclencher une erreur si vous essayez de passer un autre objet que Image
à la méthode. Très utile pour éviter de chercher des heures l'origine d'un problème parce que vous avez passé un mauvais argument. Notez également le « = null
» qui permet d'accepter les valeurs null
: rappelez-vous, la relation est facultative !
Prenez bien conscience d'une chose également : le getter getImage()
retourne une instance de la classe Image
directement. Lorsque vous avez un Article
, disons $article
, et que vous voulez récupérer l'URL de l'Image
associée, il faut donc faire :
1 2 3 4 5 6 | <?php $image = $article->getImage(); $url = $image->getUrl(); // Ou bien sûr en plus simple : $url = $article->getImage()->getUrl(); |
Pour les curieux qui auront été voir ce qui a été fait en base de données : une colonne image_id
a bien été ajouté à la table article
. Cependant, ne confondez surtout par cette colonne image_id
avec notre attribut image
, et gardez bien ces deux points en tête :
1/ L'entité Article
ne contient pas d'attribut image_id
.
2/ L'attribut image
ne contient pas l'id de l'Image
liée, il contient une instance de la classe Sdz\BlogBundle\Entity\Image
qui, elle, contient un attribut id
.
N'allez donc jamais m'écrire $article->getImageId()
, pour récupérer l'id de l'image liée, il faut d'abord récupérer l'Image elle-même puis son id, comme ceci : $article->getImage()->getId()
. Et ne faites surtout pas la confusion : une entité n'est pas une table.
Exemple d'utilisation
Pour utiliser cette relation, c'est très simple. Voici un exemple pour ajouter un nouvel Article
et son Image
depuis un contrôleur. Modifions l'action ajouterAction()
, qui était déjà bien complète :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php // N'oubliez pas de rajouter ce use ! use Sdz\BlogBundle\Entity\Image; // … public function ajouterAction() { // Création de l'entité Article $article = new Article(); $article->setTitre('Mon dernier weekend'); $article->setContenu("C'était vraiment super et on s'est bien amusé."); $article->setAuteur('winzou'); // Création de l'entité Image $image = new Image(); $image->setUrl('http://uploads.siteduzero.com/icones/478001_479000/478657.png'); $image->setAlt('Logo Symfony2'); // On lie l'image à l'article $article->setImage($image); // On récupère l'EntityManager $em = $this->getDoctrine()->getManager(); // Étape 1 : on persiste les entités $em->persist($article); // Étape 1 bis : si on n'avait pas défini le cascade={"persist"}, on devrait persister à la main l'entité $image // $em->persist($image); // Étape 2 : on déclenche l'enregistrement $em->flush(); // … reste de la méthode } |
Pour information, voici comment on pourrait modifier la vue d'un article, pour y intégrer l'image :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | {# src/Sdz/BlogBundle/Resources/views/Blog/voir.html.twig #} {% extends "SdzBlogBundle::layout.html.twig" %} {% block title %} Lecture d'un article - {{ parent() }} {% endblock %} {% block sdzblog_body %} <h2> {# On vérifie qu'une image soit bien associée à l'article #} {% if article.image is not null %} <img src="{{ article.image.url }}" alt="{{ article.image.alt }}" /> {% endif %} {{ article.titre }} </h2> <i>Par {{ article.auteur }}, le {{ article.date|date('d/m/Y') }}</i> <div class="well"> {{ article.contenu }} </div> <p> <a href="{{ path('sdzblog_accueil') }}" class="btn"> <i class="icon-chevron-left"></i> Retour à la liste </a> <a href="{{ path('sdzblog_modifier', {'id': article.id}) }}" class="btn"> <i class="icon-edit"></i> Modifier l'article </a> <a href="{{ path('sdzblog_supprimer', {'id': article.id}) }}" class="btn"> <i class="icon-trash"></i> Supprimer l'article </a> </p> {% endblock %} |
Et voici un autre exemple, qui modifierait l'Image
d'un Article
déjà existant. Ici je vais prendre une méthode arbitraire, mais vous savez tout ce qu'il faut pour l'implémenter réellement :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php // Dans un contrôleur, celui que vous voulez public function modifierImageAction($id_article) { $em = $this->getDoctrine()->getManager(); // On récupère l'article $article = $em->getRepository('SdzBlogBundle:Article')->find($id_article); // On modifie l'URL de l'image par exemple $article->getImage()->setUrl('test.png'); // On n'a pas besoin de persister notre article (si vous le faites, aucune erreur n'est déclenchée, Doctrine l'ignore) // Rappelez-vous, il l'est automatiquement car on l'a récupéré depuis Doctrine // Pas non plus besoin de persister l'image ici, car elle est également récupérée par Doctrine // On déclenche la modification $em->flush(); return new Response('OK'); } |
Le code parle de lui-même : gérer une relation est vraiment aisé avec Doctrine !
Relation Many-To-One
Présentation
La relation Many-To-One, ou n..1, est assez classique également. Elle correspond, comme son nom l'indique, à une relation qui permet à une entité A d'avoir une relation avec plusieurs entités B.
Pour illustrer cette relation dans le cadre de notre blog, nous allons créer une entité Commentaire
. L'idée est de pouvoir ajouter plusieurs commentaires à un article, et que chaque commentaire ne soit lié qu'à un seul article. Nous avons ainsi plusieurs commentaires (Many) à lier (To) à un seul article (One). La figure suivante schématise tout cela.
Comme précédemment, pour être sûrs qu'on parle bien de la même chose, créez cette entité Commentaire
avec au moins les attributs auteur
, contenu
et date
. Voici la mienne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | <?php // src/Sdz/BlogBundle/Entity/Commentaire.php namespace Sdz\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Sdz\BlogBundle\Entity\Commentaire * * @ORM\Table() * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\CommentaireRepository") */ class Commentaire { /** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string $auteur * * @ORM\Column(name="auteur", type="string", length=255) */ private $auteur; /** * @var text $contenu * * @ORM\Column(name="contenu", type="text") */ private $contenu; /** * @var datetime $date * * @ORM\Column(name="date", type="datetime") */ private $date; public function __construct() { $this->date = new \Datetime(); } /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set auteur * * @param string $auteur */ public function setAuteur($auteur) { $this->auteur = $auteur; } /** * Get auteur * * @return string */ public function getAuteur() { return $this->auteur; } /** * Set contenu * * @param text $contenu */ public function setContenu($contenu) { $this->contenu = $contenu; } /** * Get contenu * * @return text */ public function getContenu() { return $this->contenu; } /** * Set date * * @param datetime $date */ public function setDate(\Datetime $date) { $this->date = $date; } /** * Get date * * @return datetime */ public function getDate() { return $this->date; } } |
Définition de la relation dans les entités
Annotation
Pour établir cette relation dans votre entité, la syntaxe est la suivante :
Entité propriétaire, Commentaire
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php // src/Sdz/BlogBundle/Entity/Commentaire.php /** * @ORM\Entity */ class Commentaire { /** * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Article") * @ORM\JoinColumn(nullable=false) */ private $article; // … } |
Entité inverse, Article
:
1 2 3 4 5 6 7 8 9 10 11 12 | <?php // src/Sdz/BlogBundle/Entity/Article.php /** * @ORM\Entity */ class Article { // Nul besoin de rajouter de propriété, ici // … } |
L'annotation à utiliser est tout simplement ManyToOne
.
Première remarque : l'entité propriétaire pour cette relation est Commentaire
, et non Article
. Pourquoi ? Parce que, rappelez-vous, le propriétaire est celui qui contient la colonne référence. Ici, on aura bien une colonne article_id
dans la table commentaire
. En fait, de façon systématique, c'est le côté Many d'une relation Many-To-One qui est le propriétaire, vous n'avez pas le choix. Ici, on a plusieurs commentaires pour un seul article, le Many correspond aux commentaires, donc l'entité Commentaire
est la propriétaire.
Deuxième remarque : j'ai volontairement ajouté l'annotation JoinColumn
avec son attribut nullable
à false
, pour interdire la création d'un commentaire sans article. En effet, dans notre cas, un commentaire qui n'est rattaché à aucun article n'a pas de sens. Après, attention, il se peut très bien que dans votre application vous deviez laisser la possibilité au côté Many de la relation d'exister sans forcément être attaché à un côté One.
N'oubliez pas de mettre à jour la base de données avec la commande doctrine:schema:update
!
Getter et setter
Ajoutons maintenant le getter et le setter correspondants dans l'entité propriétaire. Comme tout à l'heure, vous pouvez utiliser la méthode php app/console doctrine:generate:entities SdzBlogBundle:Commentaire
, ou alors mettez ceux-là :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <?php // src/Sdz/BlogBundle/Entity/Commentaire.php /** * @ORM\Entity */ class Commentaire { /** * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Article") * @ORM\JoinColumn(nullable=false) */ private $article; // … reste des attributs /** * Set article * * @param Sdz\BlogBundle\Entity\Article $article */ public function setArticle(\Sdz\BlogBundle\Entity\Article $article) { $this->article = $article; } /** * Get article * * @return Sdz\BlogBundle\Entity\Article */ public function getArticle() { return $this->article; } // … reste des getters et setters } |
Vous pouvez remarquer que, comme notre relation est obligatoire, il n'y a pas le « = null
» dans le setArticle()
.
Exemple d'utilisation
La méthode pour gérer une relation Many-To-One n'est pas très différente de celle pour une relation One-To-One, voyez par vous-mêmes dans ces exemples.
Tout d'abord, pour ajouter un nouvel Article
et ses Commentaires
, modifions encore la méthode ajouterAction()
de notre contrôleur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php // N'oubliez pas ce use ! use Sdz\BlogBundle\Entity\Commentaire; public function ajouterAction() { // Création de l'entité Article $article = new Article(); $article->setTitre('Mon dernier weekend'); $article->setContenu("C'était vraiment super et on s'est bien amusé."); $article->setAuteur('winzou'); // Création d'un premier commentaire $commentaire1 = new Commentaire(); $commentaire1->setAuteur('winzou'); $commentaire1->setContenu('On veut les photos !'); // Création d'un deuxième commentaire, par exemple $commentaire2 = new Commentaire(); $commentaire2->setAuteur('Choupy'); $commentaire2->setContenu('Les photos arrivent !'); // On lie les commentaires à l'article $commentaire1->setArticle($article); $commentaire2->setArticle($article); // On récupère l'EntityManager $em = $this->getDoctrine()->getManager(); // Étape 1 : On persiste les entités $em->persist($article); // Pour cette relation pas de cascade, car elle est définie dans l'entité Commentaire et non Article // On doit donc tout persister à la main ici $em->persist($commentaire1); $em->persist($commentaire2); // Étape 2 : On déclenche l'enregistrement $em->flush(); // … reste de la méthode } |
Pour information, voici comment on pourrait modifier l'action voirAction()
du contrôleur pour passer non seulement l'article à la vue, mais également ses commentaires (lignes 20, 21 et 26) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php public function voirAction($id) { // On récupère l'EntityManager $em = $this->getDoctrine() ->getManager(); // On récupère l'entité correspondant à l'id $id $article = $em->getRepository('SdzBlogBundle:Article') ->find($id); if($article === null) { throw $this->createNotFoundException('Article[id='.$id.'] inexistant.'); } // On récupère la liste des commentaires $liste_commentaires = $em->getRepository('SdzBlogBundle:Commentaire') ->findAll(); // Puis modifiez la ligne du render comme ceci, pour prendre en compte l'article : return $this->render('SdzBlogBundle:Blog:voir.html.twig', array( 'article' => $article, 'liste_commentaires' => $liste_commentaires )); } |
Ici vous pouvez voir qu'on a utilisé la méthode findAll()
, qui récupère tous les commentaires et pas seulement ceux de l'article actuel. Il faudra bien sûr modifier ce comportement, nous le ferons dans le prochain chapitre, avec le repository qui permet de personnaliser nos requêtes. Et bien entendu, il faudrait adapter la vue si vous voulez afficher la liste des commentaires que nous venons de lui passer.
Relation Many-To-Many
Présentation
La relation Many-To-Many, ou n..n, correspond à une relation qui permet à plein d'objets d'être en relation avec plein d'autres !
Prenons l'exemple cette fois-ci des articles de notre blog, répartis dans des catégories. Un article peut appartenir à plusieurs catégories. À l'inverse, une catégorie peut contenir plusieurs articles. On a donc une relation Many-To-Many entre Article
et Catégorie
. La figure suivante schématise tout cela.
Cette relation est particulière dans le sens où Doctrine va devoir créer une table intermédiaire. En effet, avec la méthode traditionnelle en base de données, comment feriez-vous pour faire ce genre de relation ? Vous avez une table article
, une autre table categorie
, mais vous avez surtout besoin d'une table article_categorie
qui fait la liaison entre les deux ! Cette table de liaison ne contient que deux colonnes : article_id
et categorie_id
. Cette table intermédiaire, vous ne la connaîtrez pas : elle n’apparaît pas dans nos entités, et c'est Doctrine qui la crée et qui la gère tout seul !
Je vous ai parlé de cette table intermédiaire pour que vous compreniez comment Doctrine fonctionne. Cependant attention, nous sommes d'accord que vous devez totalement oublier cette notion de table intermédiaire lorsque vous manipulez des objets (les entités) ! J'insiste sur le fait que si vous voulez utiliser Doctrine, alors il faut le laisser gérer la base de données tout seul : vous utilisez des objets, lui utilise une base de données, chacun son travail.
Encore une fois, pour être sûrs que l'on parle bien de la même chose, créez cette entité Categorie
avec au moins un attribut nom
. Voici la mienne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | <?php // src/Sdz/BlogBundle/Entity/Categorie.php namespace Sdz\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Sdz\BlogBundle\Entity\Categorie * * @ORM\Table() * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\CategorieRepository") */ class Categorie { /** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string $nom * * @ORM\Column(name="nom", type="string", length=255) */ private $nom; /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set nom * * @param string $nom * @return Categorie */ public function setNom($nom) { $this->nom = $nom; return $this; } /** * Get nom * * @return string */ public function getNom() { return $this->nom; } } |
Définition de la relation dans les entités
Annotation
Pour établir cette relation dans vos entités, la syntaxe est la suivante.
Entité propriétaire, Article
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php // src/Sdz/BlogBundle/Entity/Article.php /** * @ORM\Entity */ class Article { /** * @ORM\ManyToMany(targetEntity="Sdz\BlogBundle\Entity\Categorie", cascade={"persist"}) */ private $categories; // … } |
Entité inverse, Categorie
:
1 2 3 4 5 6 7 8 9 10 11 12 | <?php // src/Sdz/BlogBundle/Entity/Categorie.php /** * @ORM\Entity */ class Categorie { // Nul besoin d'ajouter une propriété, ici // … } |
J'ai mis Article
comme propriétaire de la relation. C'est un choix que vous pouvez faire comme bon vous semble, ici. Mais bon, récupérer les catégories d'un article se fera assez souvent, alors que récupérer les articles d'une catégorie moins. Et puis, pour récupérer les articles d'une catégorie, on aura surement besoin de personnaliser la requête, donc on le fera de toute façon depuis le CategorieRepository.
Getter et setters
Dans ce type de relation, il faut soigner un peu plus l'entité propriétaire. Tout d'abord, on a pour la première fois un attribut (ici $categories
) qui contient une liste d'objets. C'est parce qu'il contient une liste d'objets qu'on a mis le nom de cet attribut au pluriel. Les listes d'objets avec Doctrine2 ne sont pas de simples tableaux, mais des ArrayCollection
, il faudra donc définir l'attribut comme tel dans le constructeur. Un ArrayCollection
est un objet utilisé par Doctrine2, qui a toutes les propriétés d'un tableau normal. Vous pouvez faire un foreach
dessus, et le traiter comme n'importe quel tableau. Il dispose juste de quelques méthodes supplémentaires très pratiques, que nous verrons.
Ensuite, le getter est classique et s'appelle getCategories()
. Par contre, c'est les setters qui vont différer un peu. En effet, $categories
est une liste de catégories, mais au quotidien ce qu'on va faire c'est ajouter une à une des catégories à cette liste. Il nous faut donc une méthode addCategorie()
(sans « s », on n'ajoute qu'une seule catégorie à la fois) et non une setCategories()
. Du coup, il nous faut également une méthode pour supprimer une catégorie de la liste, que l'on appelle removeCategorie()
.
Ajoutons maintenant le getter et les setters correspondants dans l'entité propriétaire, Article
. Comme tout à l'heure, vous pouvez utiliser la méthode php app/console doctrine:generate:entities SdzBlogBundle:Article
, ou alors reprendre ce code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | <?php // src/Sdz/BlogBundle/Entity/Article.php /** * @ORM\Entity */ class Article { /** * @ORM\ManyToMany(targetEntity="Sdz\BlogBundle\Entity\Categorie", cascade={"persist"}) */ private $categories; // … vos autres attributs // Comme la propriété $categories doit être un ArrayCollection, souvenez-vous // On doit la définir dans un constructeur : public function __construct() { // Si vous aviez déjà un constructeur, ajoutez juste cette ligne : $this->categories = new \Doctrine\Common\Collections\ArrayCollection(); } /** * Add categories * * @param Sdz\BlogBundle\Entity\Categorie $categories */ public function addCategorie(\Sdz\BlogBundle\Entity\Categorie $categorie) // addCategorie sans « s » ! { // Ici, on utilise l'ArrayCollection vraiment comme un tableau, avec la syntaxe [] $this->categories[] = $categorie; } /** * Remove categories * * @param Sdz\BlogBundle\Entity\Categorie $categories */ public function removeCategorie(\Sdz\BlogBundle\Entity\Categorie $categorie) // removeCategorie sans « s » ! { // Ici on utilise une méthode de l'ArrayCollection, pour supprimer la catégorie en argument $this->categories->removeElement($categorie); } /** * Get categories * * @return Doctrine\Common\Collections\Collection */ public function getCategories() // Notez le « s », on récupère une liste de catégories ici ! { return $this->categories; } // … vos autres getters/setters } |
N'oubliez pas de mettre à jour la base de données avec la commande doctrine:schema:update
.
Remplissons la base de données avec les fixtures
Avant de voir un exemple, j'aimerais vous faire ajouter quelques catégories en base de données, histoire d'avoir de quoi jouer avec. Pour cela, petit aparté, nous allons faire une fixture Doctrine ! Cela va nous permettre d'utiliser le bundle qu'on a installé lors du chapitre sur Composer.
Les fixtures Doctrine permettent de remplir la base de données avec un jeu de données que nous allons définir. Cela permet de pouvoir tester avec des vraies données, sans devoir les retaper à chaque fois : on les inscrit une fois pour toutes, et ensuite elles sont toutes insérées en base de données en une seule commande.
Tout d'abord, créons notre fichier de fixture pour l'entité Categorie
. Les fixtures d'un bundle se trouvent dans le répertoire DataFixtures/ORM
(ou ODM
pour des documents). Voici à quoi ressemble notre fixture Categories
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <?php // src/Sdz/BlogBundle/DataFixtures/ORM/Categories.php namespace Sdz\BlogBundle\DataFixtures\ORM; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Common\Persistence\ObjectManager; use Sdz\BlogBundle\Entity\Categorie; class Categories implements FixtureInterface { // Dans l'argument de la méthode load, l'objet $manager est l'EntityManager public function load(ObjectManager $manager) { // Liste des noms de catégorie à ajouter $noms = array('Symfony2', 'Doctrine2', 'Tutoriel', 'Évènement'); foreach($noms as $i => $nom) { // On crée la catégorie $liste_categories[$i] = new Categorie(); $liste_categories[$i]->setNom($nom); // On la persiste $manager->persist($liste_categories[$i]); } // On déclenche l'enregistrement $manager->flush(); } } |
C'est tout ! On peut dès à présent insérer ces données dans la base de données. Voici donc la commande à exécuter :
1 2 3 4 | C:\wamp\www\Symfony>php app/console doctrine:fixtures:load Careful, database will be purged. Do you want to continue Y/N ?y > purging database > loading Sdz\BlogBundle\DataFixtures\ORM\Categories |
Et voilà ! Les quatre catégories définies dans le fichier de fixture sont maintenant enregistrées en base de données, on va pouvoir s'en servir dans nos exemples. Par la suite, on rajoutera d'autres fichiers de fixture pour insérer d'autres entités en base de données : la commande les traitera tous l'un après l'autre.
Attention, comme vous avez pu le voir, l'exécution de la commande Doctrine pour insérer les fixtures vide totalement la base de données avant d'insérer les nouvelles données. Si vous voulez ajouter les fixtures en plus des données déjà présentes, il faut ajouter l'option --append
à la commande précédente. Cependant, c'est rarement ce que vous voulez, car à la prochaine exécution des fixtures, vous allez insérer une nouvelle fois les mêmes catégories…
Exemples d'utilisation
Voici un exemple pour ajouter un article existant à plusieurs catégories existantes. Je vous propose de mettre ce code dans notre méthode modifierAction()
par exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php class BlogController extends Controller { // … // Ajout d'un article existant à plusieurs catégories existantes : public function modifierAction($id) { // On récupère l'EntityManager $em = $this->getDoctrine() ->getManager(); // On récupère l'entité correspondant à l'id $id $article = $em->getRepository('SdzBlogBundle:Article') ->find($id); if ($article === null) { throw $this->createNotFoundException('Article[id='.$id.'] inexistant.'); } // On récupère toutes les catégories : $liste_categories = $em->getRepository('SdzBlogBundle:Categorie') ->findAll(); // On boucle sur les catégories pour les lier à l'article foreach($liste_categories as $categorie) { $article->addCategorie($categorie); } // Inutile de persister l'article, on l'a récupéré avec Doctrine // Étape 2 : On déclenche l'enregistrement $em->flush(); return new Response('OK'); // … reste de la méthode } } |
Je vous mets un exemple concret d'application pour que vous puissiez vous représenter l'utilisation de la relation dans un vrai cas d'utilisation. Les seules lignes qui concernent vraiment l'utilisation de notre relation Many-To-Many sont les lignes 29 à 32 : la boucle sur les catégories pour ajouter chaque catégorie une à une à l'article en question.
Voici un autre exemple pour enlever toutes les catégories d'un article. Modifions la méthode supprimerAction()
pour l'occasion :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php class BlogController extends Controller { // … // Suppression des catégories d'un article : public function supprimerAction($id) { // On récupère l'EntityManager $em = $this->getDoctrine() ->getManager(); // On récupère l'entité correspondant à l'id $id $article = $em->getRepository('SdzBlogBundle:Article') ->find($id); if ($article === null) { throw $this->createNotFoundException('Article[id='.$id.'] inexistant.'); } // On récupère toutes les catégories : $liste_categories = $em->getRepository('SdzBlogBundle:Categorie') ->findAll(); // On enlève toutes ces catégories de l'article foreach($liste_categories as $categorie) { // On fait appel à la méthode removeCategorie() dont on a parlé plus haut // Attention ici, $categorie est bien une instance de Categorie, et pas seulement un id $article->removeCategorie($categorie); } // On n'a pas modifié les catégories : inutile de les persister // On a modifié la relation Article - Categorie // Il faudrait persister l'entité propriétaire pour persister la relation // Or l'article a été récupéré depuis Doctrine, inutile de le persister // On déclenche la modification $em->flush(); return new Response('OK'); } } |
Encore une fois, je vous ai mis un code complet, mais ce qui nous intéresse dans le cadre de la relation, ce ne sont que les lignes 30 à 35.
Enfin, voici un dernier exemple pour afficher les catégories d'un article dans la vue. Regardez plus particulièrement les lignes 21 à 27 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | {# src/Sdz/BlogBundle/Resources/views/Blog/voir.html.twig #} {% extends "SdzBlogBundle::layout.html.twig" %} {% block title %} Lecture d'un article - {{ parent() }} {% endblock %} {% block sdzblog_body %} <h2> {# On vérifie qu'une image est bien associée à l'article #} {% if article.image is not null %} <img src="{{ article.image.url }}" alt="{{ article.image.alt }}" /> {% endif %} {{ article.titre }} </h2> <i>Par {{ article.auteur }}, le {{ article.date|date('d/m/Y') }}</i> {% if article.categories.count > 0 %} - Catégories : {% for categorie in article.categories %} {{ categorie.nom }} {% if not loop.last %} - {% endif %} {% endfor %} {% endif %} <div class="well"> {{ article.contenu }} </div> <p> <a href="{{ path('sdzblog_accueil') }}" class="btn"> <i class="icon-chevron-left"></i> Retour à la liste </a> <a href="{{ path('sdzblog_modifier', {'id': article.id}) }}" class="btn"> <i class="icon-edit"></i> Modifier l'article </a> <a href="{{ path('sdzblog_supprimer', {'id': article.id}) }}" class="btn"> <i class="icon-trash"></i> Supprimer l'article </a> </p> {% endblock %} |
Vous voyez qu'on accède aux catégories d'un article grâce à l'attribut categories
de l'article, tout simplement. En Twig, cela signifie {{ article.categories }}
, en PHP on ferait $article->getCategories()
. Il suffit ensuite de parcourir ce tableau grâce à une boucle, et d'en faire ce que l'on veut.
Relation Many-To-Many avec attributs
Présentation
La relation Many-To-Many qu'on vient de voir peut suffire dans bien des cas, mais elle est en fait souvent incomplète pour les besoins d'une application.
Pour illustrer ce manque, rien de tel qu'un exemple : considérons l'entité Produit
d'un site e-commerce ainsi que l'entité Commande
. Une commande contient plusieurs produits, et bien entendu un même produit peut être dans différentes commandes. On a donc bien une relation Many-To-Many. Voyez-vous le manque ? Lorsqu'un utilisateur ajoute un produit à une commande, où met-on la quantité de ce produit qu'il veut ? Si je veux 3 exemplaires de Harry Potter, où mettre cette quantité ? Dans l'entité Commande
? Non cela n'a pas de sens. Dans l'entité Produit
? Non, cela n'a pas de sens non plus. Cette quantité est un attribut de la relation qui existe entre Produit
et Commande
, et non un attribut de Produit
ou de Commande
.
Il n'y a pas de moyen simple de gérer les attributs d'une relation avec Doctrine. Pour cela, il faut esquiver en créant simplement une entité intermédiaire qui va représenter la relation, appelons-la CommandeProduit
. Et c'est dans cette entité que l'on mettra les attributs de relation, comme notre quantité. Ensuite, il faut bien entendu mettre en relation cette entité intermédiaire avec les deux autres entités d'origine, Commande
et Produit
. Pour cela, il faut logiquement faire : Commande
One-To-Many CommandeProduit
Many-To-One Produit
. En effet, une commande (One) peut avoir plusieurs relations avec des produits (Many), plusieurs CommandeProduit
, donc ! La relation est symétrique pour les produits.
Attention, dans le titre de cette section, j'ai parlé de la relation Many-To-Many avec attributs, mais il s'agit bien en fait de deux relations Many-To-One des plus normales, soyons d'accord. On ne va donc rien apprendre dans ce prochain paragraphe, car on sait déjà faire une Many-To-One, mais c'est une astuce qu'il faut bien connaître et savoir utiliser, donc prenons le temps de bien la comprendre.
J'ai pris l'exemple de produits et de commandes, car c'est plus intuitif pour comprendre l'enjeu et l'utilité de cette relation. Cependant, pour rester dans le cadre de notre blog, on va faire une relation entre des Article
et des Compétence
, et l'attribut de la relation sera le niveau. L'idée est de pouvoir afficher sur chaque article la liste des compétences utilisées (Symfony2, Doctrine2, Formulaire, etc.) avec le niveau dans chaque compétence (Débutant, Avisé et Expert). On a alors l'analogie suivante :
Article
<=>Commande
ArticleCompetence
<=>Commande_Produit
Competence
<=>Produit
Et donc : Article
One-To-Many ArticleCompetence
Many-To-One Competence
.
Pour cela, créez d'abord cette entité Compétence
, avec au moins un attribut nom
. Voici la mienne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | <?php // src/Sdz/BlogBundle/Entity/Competence.php namespace Sdz\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Sdz\BlogBundle\Entity\Competence * * @ORM\Table() * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\CompetenceRepository") */ class Competence { /** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string $nom * * @ORM\Column(name="nom", type="string", length=255) */ private $nom; /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set nom * * @param string $nom * @return Competence */ public function setNom($nom) { $this->nom = $nom; } /** * Get nom * * @return string */ public function getNom() { return $this->nom; } } |
Définition de la relation dans les entités
Annotation
Tout d'abord, on va créer notre entité de relation (notre ArticleCompetence
) comme ceci :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <?php // src/Sdz/BlogBundle/Entity/ArticleCompetence.php namespace Sdz\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class ArticleCompetence { /** * @ORM\Id * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Article") */ private $article; /** * @ORM\Id * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Competence") */ private $competence; /** * @ORM\Column() */ private $niveau; // Ici j'ai un attribut de relation « niveau » // … vous pouvez ajouter d'autres attributs bien entendu } |
Comme les côtés Many des deux relations Many-To-One sont dans ArticleCompetence
, cette entité est l'entité propriétaire des deux relations.
Et ces @ORM\Id
? Pourquoi y en a-t-il deux et qu'est-ce qu'ils viennent faire ici ?
Très bonne question. Comme toute entité, notre ArticleCompetence
se doit d'avoir un identifiant. C'est obligatoire pour que Doctrine puisse la gérer par la suite. Depuis le début, nous avons ajouté un attribut id qui était en auto-incrément, on ne s'en occupait pas trop donc. Ici c'est différent, comme une ArticleCompetence
correspond à un unique couple Article/Competence
(pour chaque couple Article/Competence
, il n'y a qu'une seule ArticleCompetence
), on peut se servir de ces deux attributs pour former l'identifiant de cette entité.
Pour cela, il suffit de définir @ORM\Id
sur les deux colonnes, et Doctrine saura mettre une clé primaire sur ces deux colonnes, puis les gérer comme n'importe quel autre identifiant. Encore une fois, merci Doctrine !
Mais, avec une relation unidirectionnelle, on ne pourra pas faire $article->getArticleCompetence()
pour récupérer les ArticleCompetence
et donc les compétences ? Ni l'inverse depuis $competence
?
En effet, et c'est pourquoi la prochaine section de ce chapitre traite des relations bidirectionnelles ! En attendant, pour notre relation One-To-Many-To-One, continuons simplement sur une relation unidirectionnelle.
Sachez quand même que vous pouvez éviter une relation bidirectionnelle ici en utilisant simplement la méthode findByCommande($commande->getId())
(pour récupérer les produits d'une commande) ou findByProduit($produit->getId())
(pour l'inverse) du repository CommandeProduitRepository.
L'intérêt de la bidirectionnelle ici est lorsque vous voulez afficher une liste des commandes avec leurs produits. Dans la boucle sur les commandes, vous n'allez pas faire appel à une méthode du repository qui va générer une requête par boucle, il faudra passer par un $commande->getCommandeProduits()
. Nous le verrons plus loin dans ce chapitre.
N'oubliez pas de mettre à jour votre base de données en exécutant la commande doctrine:schema:update
.
Getters et setters
Comme d'habitude les getters et setters doivent se définir dans l'entité propriétaire. Ici, rappelez-vous, nous sommes en présence de deux relations Many-To-One dont la propriétaire est l'entité ArticleCompetence
. Nous avons donc deux getters et deux setters classiques à écrire. Vous pouvez les générer avec la commande doctrine:generate:entities SdzBlogBundle:ArticleCompetence
, ou mettre le code suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | <?php // src/Sdz/BlogBundle/Entity/ArticleCompetence.php namespace Sdz\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class ArticleCompetence { /** * @ORM\Id * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Article") */ private $article; /** * @ORM\Id * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Competence") */ private $competence; /** * @ORM\Column() */ private $niveau; // Ici j'ai un attribut de relation « niveau » // … les autres attributs // Getter et setter pour l'entité Article public function setArticle(\Sdz\BlogBundle\Entity\Article $article) { $this->article = $article; } public function getArticle() { return $this->article; } // Getter et setter pour l'entité Competence public function setCompetence(\Sdz\BlogBundle\Entity\Competence $competence) { $this->competence = $competence; } public function getCompetence() { return $this->competence; } // On définit le getter/setter de l'attribut « niveau » public function setNiveau($niveau) { $this->niveau = $niveau; } public function getNiveau() { return $this->niveau; } // … les autres getters/setters si vous en avez } |
Remplissons la base de données
Comme précédemment, on va d'abord ajouter des compétences en base de données grâce aux fixtures. Pour faire une nouvelle fixture, il suffit de créer un nouveau fichier dans le répertoire DataFixtures/ORM
dans le bundle. Je vous invite à créer le fichier src/Sdz/BlogBundle/DataFixtures/ORM/Competences.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <?php // src/Sdz/BlogBundle/DataFixtures/ORM/Competences.php namespace Sdz\BlogBundle\DataFixtures\ORM; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Common\Persistence\ObjectManager; use Sdz\BlogBundle\Entity\Competence; class Competences implements FixtureInterface { public function load(ObjectManager $manager) { // Liste des noms de compétences à ajouter $noms = array('Doctrine', 'Formulaire', 'Twig'); foreach($noms as $i => $nom) { // On crée la compétence $liste_competences[$i] = new Competence(); $liste_competences[$i]->setNom($nom); // On la persiste $manager->persist($liste_competences[$i]); } // On déclenche l'enregistrement $manager->flush(); } } |
Et maintenant, on peut exécuter la commande :
1 2 3 4 5 | C:\wamp\www\Symfony>php app/console doctrine:fixtures:load Careful, database will be purged. Do you want to continue Y/N ?y > purging database > loading Sdz\BlogBundle\DataFixtures\ORM\Categories > loading Sdz\BlogBundle\DataFixtures\ORM\Competences |
Vous pouvez voir qu'après avoir tout vidé Doctrine a inséré les fixtures Categories
puis nos fixtures Competences
. Tout est prêt !
Exemple d'utilisation
La manipulation des entités dans une telle relation est un peu plus compliquée, surtout sans la bidirectionnalité. Mais on peut tout de même s'en sortir. Tout d'abord, voici un exemple pour créer un nouvel article contenant plusieurs compétences ; mettons ce code dans la méthode ajouterAction()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php // N'oubliez pas ce use évidemment use Sdz\BlogBundle\Entity\ArticleCompetence; class BlogController extends Controller { // … public function ajouterAction() { // On récupére l'EntityManager $em = $this->getDoctrine() ->getManager(); // Création de l'entité Article $article = new Article(); $article->setTitre('Mon dernier weekend'); $article->setContenu("C'était vraiment super et on s'est bien amusé."); $article->setAuteur('winzou'); // Dans ce cas, on doit créer effectivement l'article en bdd pour lui assigner un id // On doit faire cela pour pouvoir enregistrer les ArticleCompetence par la suite $em->persist($article); $em->flush(); // Maintenant, $article a un id défini // Les compétences existent déjà, on les récupère depuis la bdd $liste_competences = $em->getRepository('SdzBlogBundle:Competence') ->findAll(); // Pour l'exemple, notre Article contient toutes les Competences // Pour chaque compétence foreach($liste_competences as $i => $competence) { // On crée une nouvelle « relation entre 1 article et 1 compétence » $articleCompetence[$i] = new ArticleCompetence; // On la lie à l'article, qui est ici toujours le même $articleCompetence[$i]->setArticle($article); // On la lie à la compétence, qui change ici dans la boucle foreach $articleCompetence[$i]->setCompetence($competence); // Arbitrairement, on dit que chaque compétence est requise au niveau 'Expert' $articleCompetence[$i]->setNiveau('Expert'); // Et bien sûr, on persiste cette entité de relation, propriétaire des deux autres relations $em->persist($articleCompetence[$i]); } // On déclenche l'enregistrement $em->flush(); // … reste de la méthode } } |
Et voici un autre exemple pour récupérer les compétences et leur niveau à partir d'un article donné. Je vous propose de modifier la méthode voirAction()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php class BlogController extends Controller { // … public function voirAction($id) { // On récupère l'EntityManager $em = $this->getDoctrine() ->getManager(); // On récupère l'entité correspondant à l'id $id $article = $em->getRepository('SdzBlogBundle:Article') ->find($id); if ($article === null) { throw $this->createNotFoundException('Article[id='.$id.'] inexistant.'); } // On récupère les articleCompetence pour l'article $article $liste_articleCompetence = $em->getRepository('SdzBlogBundle:ArticleCompetence') ->findByArticle($article->getId()); // Puis modifiez la ligne du render comme ceci, pour prendre en compte les articleCompetence : return $this->render('SdzBlogBundle:Blog:voir.html.twig', array( 'article' => $article, 'liste_articleCompetence' => $liste_articleCompetence, // … et évidemment les autres variables que vous pouvez avoir )); } } |
Et le code de la vue correspondante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | {# src/Sdz/BlogBundle/Resources/views/Blog/voir.html.twig #} {% extends "SdzBlogBundle::layout.html.twig" %} {% block title %} Lecture d'un article - {{ parent() }} {% endblock %} {% block sdzblog_body %} <h2> {# On vérifie qu'une image est bien associée à l'article #} {% if article.image is not null %} <img src="{{ article.image.url }}" alt="{{ article.image.alt }}" /> {% endif %} {{ article.titre }} </h2> <i>Par {{ article.auteur }}, le {{ article.date|date('d/m/Y') }}</i> {% if article.categories.count > 0 %} - Catégories : {% for categorie in article.categories %} {{ categorie.nom }} {% if not loop.last %} - {% endif %} {% endfor %} {% endif %} <div class="well"> {{ article.contenu }} </div> {% if liste_articleCompetence|length > 0 %} <div> Compétences utilisées dans cet article : <ul> {% for articleCompetence in liste_articleCompetence %} <li>{{ articleCompetence.competence.nom }} : {{ articleCompetence.niveau }}</li> {% endfor %} </ul> </div> {% endif %} <p> <a href="{{ path('sdzblog_accueil') }}" class="btn"> <i class="icon-chevron-left"></i> Retour à la liste </a> <a href="{{ path('sdzblog_modifier', {'id': article.id}) }}" class="btn"> <i class="icon-edit"></i> Modifier l'article </a> <a href="{{ path('sdzblog_supprimer', {'id': article.id}) }}" class="btn"> <i class="icon-trash"></i> Supprimer l'article </a> </p> {% endblock %} |
C'est un exemple simple bien sûr.
Attention, dans cet exemple, la méthode findByArticle()
utilisée dans le contrôleur ne sélectionne que les ArticleCompetence
. Donc, lorsque dans la boucle dans la vue on fait {{ articleCompetence.competence }}
, en réalité Doctrine va effectuer une requête pour récupérer la Competence
associée à cette ArticleCompetence
. C'est bien sûr une horreur, car il va faire une requête… par itération dans le for
! Si vous avez 20 compétences attachées à l'article, cela ferait 20 requêtes : inimaginable.
Pour charger les Competence
en même temps que les ArticleCompetence
dans le contrôleur, et ainsi ne plus faire de requête dans la boucle, il faut faire une méthode à nous dans le repository de ArticleCompetence
. On voit tout cela dans le chapitre suivant dédié aux repositories. N'utilisez donc jamais cette technique, attendez le prochain chapitre ! La seule différence dans le contrôleur sera d'utiliser une autre méthode que findByArticle()
, et la vue ne changera même pas.
Les relations bidirectionnelles
Présentation
Vous avez vu que jusqu'ici nous n'avons jamais modifié l'entité inverse d'une relation, mais seulement l'entité propriétaire. Toutes les relations que l'on vient de faire sont donc des relations unidirectionnelles.
Leur avantage est de définir la relation d'une façon très simple. Mais l'inconvénient est de ne pas pouvoir récupérer l'entité propriétaire depuis l'entité inverse, le fameux <?php $entiteInverse->getEntiteProprietaire()
(pour nous, <?php $article->getCommentaires()
par exemple). Je dis inconvénient, mais vous avez pu constater que cela ne nous a pas du tout empêché de faire ce qu'on voulait ! À chaque fois, on a réussi à ajouter, lister, modifier nos entités et leurs relations.
Mais dans certains cas, avoir une relation bidirectionnelle est bien utile. Nous allons les voir rapidement dans cette section. Sachez que la documentation l'explique également très bien : vous pourrez vous renseigner sur le chapitre sur la création des relations, puis celui sur leur utilisation.
Définition de la relation dans les entités
Pour étudier la définition d'une relation bidirectionnelle, nous allons étudier une relation Many-To-One. Souvenez-vous bien de cette relation, dans sa version unidirectionnelle, pour pouvoir attaquer sa version bidirectionnelle dans les meilleures conditions.
Nous allons ici construire une relation bidirectionnelle de type Many-To-One, basée sur notre exemple Article-Commentaire
. Mais la méthode est exactement la même pour les relations de type One-To-One ou Many-To-Many.
Je pars du principe que vous avez déjà une relation unidirectionnelle fonctionnelle. Si ce n'est pas votre cas, mettez-la en place avant de lire la suite du paragraphe, car nous n'allons voir que les ajouts à faire.
Annotation
Alors, attaquons la gestion d'une relation bidirectionnelle. L'objectif de cette relation est de rendre possible l'accès à l'entité propriétaire depuis l'entité inverse. Avec une unidirectionnelle, cela n'est pas possible, car on n'ajoute pas d'attribut dans l'entité inverse, ce qui signifie que l'entité inverse ne sait même pas qu'elle fait partie d'une relation.
La première étape consiste donc à rajouter un attribut, et son annotation, à notre entité inverse Article
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php // src/Sdz/BlogBundle/Entity/Article.php /** * @ORM\Entity */ class Article { /** * @ORM\OneToMany(targetEntity="Sdz\BlogBundle\Entity\Commentaire", mappedBy="article") */ private $commentaires; // Ici commentaires prend un « s », car un article a plusieurs commentaires ! // … } |
Bien entendu, je vous dois des explications sur ce que l'on vient de faire.
Commençons par l'annotation. L'inverse d'un Many-To-One est… un One-To-Many, tout simplement ! Il faut donc utiliser l'annotation One-To-Many dans l'entité inverse. Je rappelle que le propriétaire d'une relation Many-To-One est toujours le côté Many, donc, lorsque vous voyez l'annotation Many-To-One, vous êtes forcément du côté propriétaire. Ici on a un One-To-Many, on est bien du côté inverse.
Ensuite, les paramètres de cette annotation. Le targetEntity
est évident, il s'agit toujours de l'entité à l'autre bout de la relation, ici notre entité Commentaire
. Le mappedBy
correspond, lui, à l'attribut de l'entité propriétaire (Commentaire
) qui pointe vers l'entité inverse (Article
) : c'est le private $article
de l'entité Commentaire
. Il faut le renseigner pour que l'entité inverse soit au courant des caractéristiques de la relation : celles-ci sont définies dans l'annotation de l'entité propriétaire.
Il faut également adapter l'entité propriétaire, pour lui dire que maintenant la relation est de type bidirectionnelle et non plus unidirectionnelle. Pour cela, il faut rajouter le paramètre inversedBy
dans l'annotation Many-To-One :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php // src/Sdz/BlogBundle/Entity/Commentaire.php /** * @ORM\Entity */ class Commentaire { /** * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Article", inversedBy="commentaires") * @ORM\JoinColumn(nullable=false) */ private $article; // … } |
Ici, nous avons seulement rajouté le paramètre inversedBy
. Il correspond au symétrique du mappedBy
, c'est-à-dire à l'attribut de l'entité inverse (Article
) qui pointe vers l'entité propriétaire (Commentaire
). C'est donc l'attribut commentaires
.
Tout est bon côté annotation, maintenant il faut également ajouter les getters et setters dans l'entité inverse bien entendu.
Getters et setters
On part d'une relation unidirectionnelle fonctionnelle, donc les getters et setters de l'entité propriétaire sont bien définis.
Dans un premier temps, ajoutons assez logiquement le getter et le setter dans l'entité inverse. On vient de lui ajouter un attribut, il est normal que le getter et le setter aillent de paire. Comme nous sommes du côté One d'un One-To-Many, l'attribut commentaires
est un ArrayCollection
. C'est donc un addCommentaire / removeCommentaire / getCommentaires
qu'il nous faut. Encore une fois, vous pouvez le générer avec doctrine:generate:entities SdzBlogBundle:Article
, ou alors vous pouvez mettre ce code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <?php // src/Sdz/BlogBundle/Entity/Article.php /** * @ORM\Entity */ class Article { /** * @ORM\OneToMany(targetEntity="Sdz\BlogBundle\Entity\Commentaire", mappedBy="article") */ private $commentaires; // Ici commentaires prend un « s », car un article a plusieurs commentaires ! // … vos autres attributs public function __construct() { // Rappelez-vous, on a un attribut qui doit contenir un ArrayCollection, on doit l'initialiser dans le constructeur $this->commentaires = new \Doctrine\Common\Collections\ArrayCollection(); } public function addCommentaire(\Sdz\BlogBundle\Entity\Commentaire $commentaire) { $this->commentaires[] = $commentaire; return $this; } public function removeCommentaire(\Sdz\BlogBundle\Entity\Commentaire $commentaire) { $this->commentaires->removeElement($commentaire); } public function getCommentaires() { return $this->commentaires; } // … } |
Maintenant, il faut nous rendre compte d'un petit manque. Voici une petite problématique, lisez bien ce code :
1 2 3 4 5 6 7 | <?php // Création des entités $article = new Article; $commentaire = new Commentaire; // On lie le commentaire à l'article $article->addCommentaire($commentaire); |
Que retourne $commentaire->getArticle()
?
Rien ! En effet, pour qu'un $commentaire->getArticle()
retourne un article, il faut d'abord le lui définir en appelant $commentaire->setArticle($article)
, c'est logique !
Si vous ne voyez pas pourquoi Doctrine n'a pas rempli l'attribut article
de l'objet $commentaire
, il faut revenir aux fondamentaux. Vous êtes en train d'écrire du PHP, $article
et $commentaire
sont des objets PHP, tels qu'ils existaient bien avant la naissance de Doctrine. Pour que l'attribut article
soit défini, il faut absolument faire appel au setter setArticle()
, car c'est le seul qui accède à cet attribut (qui est en private
). Dans le petit exemple que je vous ai mis, on n'a pas exécuté de fonction Doctrine : à aucun moment il n'a pu intervenir et éventuellement exécuter le setArticle()
.
C'est logique en soi, mais du coup dans notre code cela va être moins beau : il faut en effet lier le commentaire à l'article et l'article au commentaire. Comme ceci :
1 2 3 4 5 6 7 8 9 10 | <?php // Création des entités $article = new Article; $commentaire = new Commentaire; // On lie le commentaire à l'article $article->addCommentaire($commentaire); // On lie l'article au commentaire $commentaire->setArticle($article); |
Mais ces deux méthodes étant intimement liées, on doit en fait les imbriquer. En effet, laisser le code en l'état est possible, mais imaginez qu'un jour vous oubliiez d'appeler l'une des deux méthodes ; votre code ne sera plus cohérent. Et un code non cohérent est un code qui a des risques de contenir des bugs. La bonne méthode est donc simplement de faire appel à l'une des méthodes depuis l'autre. Voici concrètement comme le faire en modifiant les setters dans l'une des deux entités :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?php // src/Sdz/BlogBundle/Entity/Article /** * @ORM\Entity */ class Article { // … public function addCommentaire(\Sdz\BlogBundle\Entity\Commentaire $commentaire) { $this->commentaires[] = $commentaire; $commentaires->setArticle($this); // On ajoute ceci return $this; } public function removeCommentaire(\Sdz\BlogBundle\Entity\Commentaire $commentaire) { $this->commentaires->removeElement($commentaire); // Et si notre relation était facultative (nullable=true, ce qui n'est pas notre cas ici attention) : // $commentaire->setArticle(null); } // … } |
Notez qu'ici j'ai modifié un côté de la relation (l'inverse en l'occurrence), mais surtout pas les deux ! En effet, si addCommentaire()
exécute setArticle()
, qui exécute à son tour addCommentaire()
, qui… etc. On se retrouve avec une boucle infinie.
Bref, l'important est de se prendre un côté (propriétaire ou inverse, cela n'a pas d'importance), et de l'utiliser. Par utiliser, j'entends que dans le reste du code (contrôleur, service, etc.) il faudra exécuter $article->addCommentaire()
qui garde la cohérence entre les deux entités. Il ne faudra pas exécuter $commentaire->setArticle()
, car lui ne garde pas la cohérence ! Retenez : on modifie le setter d'un côté, et on utilise ensuite ce setter-là. C'est simple, mais important à respecter.
Pour conclure
Le chapitre sur les relations Doctrine touche ici à sa fin.
Pour maîtriser les relations que nous venons d'apprendre, il faut vous entraîner à les créer et à les manipuler. N'hésitez donc pas à créer des entités d'entraînement, et à voir leur comportement dans les relations.
Si vous voulez plus d'informations sur les fixtures que l'on a rapidement abordées lors de ce chapitre, je vous invite à lire la page de la documentation du bundle : http://symfony.com/fr/doc/current/bundles/DoctrineFixturesBundle/index.html
Rendez-vous au prochain chapitre pour apprendre à récupérer les entités depuis la base de données à votre guise, grâce aux repositories !
En résumé
- Les relations Doctrine révèlent toute la puissance de l'ORM ;
- Dans une relation entre deux entités, l'une est propriétaire de la relation et l'autre est inverse. Cette notion est purement technique ;
- Une relation est dite unidirectionnelle si l'entité inverse n'a pas d'attribut la liant à l'entité propriétaire. On met en place une relation bidirectionnelle lorsqu'on a besoin de cet attribut dans l'entité inverse (ce qui arrivera pour certains formulaires, etc.).