Aller au menu - Aller au contenu

Icône Les relations entre entités avec Doctrine2

Avatar
Mise à jour : 02/05/2012
Difficulté : Difficile Difficile Durée d'étude : 4 heures Creative Commons BY-NC-SA
20 752 visites depuis 7 jours, dont 818 sur ce chapitre classé 17/786
Dans ce chapitre nous allons apprendre à mettre en relation nos entités, afin de créer un ensemble cohérent.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

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é ! 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(). Et bien c'est cela que nous allons faire ici !

Les différents types de relations


Il y a plusieurs façon de lier des entités entre elles. En effet il n'est pas pareil de lier une multitudes de commentaires à un seul article, que 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é à 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 », très bien. 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 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 sommes, on se garde le choix de charger ou non la relation.

Mais nous verrons tout cela dans le prochain chapitre sur les Repository. 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. Ainsi, dans une application avec des clients et leur adresse, la relation entre Client et Adresse est une relation One-To-One. En effet, un client correspond à une seule adresse, et une adresse correspond à un seul client.

Définition de la relation dans les entités


Annotation


Pour établir cette relation dans votre entité, la syntaxe est la suivante :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
/**
 * @ORM\Entity
 */
class Client
{
    /**
     * @ORM\OneToOne(targetEntity="Namespace\Bundle\Entity\Adresse")
     */
    private $adresse;

    // ...
}

/**
 * @ORM\Entity
 */
class Adresse
{
    // Nul besoin de rajouter une propriété ici.

    // ...
}

La définition de la relation est plutôt simple, mais détaillons la bien. Tout d'abord, seule l'entité propriétaire a été modifiée, ici Client. En effet, l'entité inverse ne sait en fait même pas qu'elle est liée à une autre entité, ce n'est pas son rôle. Ensuite, l'annotation pour définir la relation est le OneToOne, dont les arguments sont :
  • targetEntity, il définit quelle est l'entité à l'autre bout de la relation, en utilisant son namespace complet.
  • cascade, il définit quelles sont les opérations à répercuter sur cette entité à l'autre bout de la relation. Par défaut, rien n'est répercuté. On n'a pas utilisé cet argument cascade dans l'exemple, mais sachez qu'il s'utilise comme ceci :
    Code : Autre
    1
    
    @ORM\OneToOne(targetEntity="Namespace\Bundle\Entity\Adresse", cascade={"remove", "persist"})

    Les arguments les plus souvent utilisés sont :
    • Persist : Cela permet de ne pas devoir faire plusieurs persist lors de nos manipulation. Sans ce persist, pour enregistrer une nouvelle entité propriétaire et une nouvelle entité inverse, il nous faut faire deux persist() : un pour chaque entité. Avec cet argument persist, un seul persist() sur l'entité propriétaire est nécessaire, l'entité inverse est alors automatiquement persistée par Doctrine.
    • Remove : Cela permet de supprimer automatiquement l'entité inverse lorsqu'on supprime l'entité propriétaire. Dans notre exemple, si on supprime un Client alors l'Adresse est automatiquement supprimée : cela empêche d'avoir une Adresse orpheline.
    • Il en existe deux autres, merge et detach, moins utilisés mais bien documenté : n'hésitez pas à aller voir la documentation.

Nous n'utiliserons pas de cascade au cours de ce tutoriel pour bien prendre l'habitude d'appliquer des persist() à nos entités. Mais également parce qu'ils nécessitent souvent une relation bidirectionnelle pour avoir du sens (pas de soucis pour une One-To-One, mais cela posera problème pour une Many-To-One).

Vous pouvez voir que j'ai défini Client comme l'entité propriétaire de la relation (car c'est lui qui contient la référence). Adresse est donc l'inverse. Dans une relation OneToOne le côté propriétaire est assez arbitraire, mais le mieux est de choisir celui qui est le plus important. Ici par exemple, les clients sont plus importants que les adresses ; une adresse est une information sur un client.

Getter et setter


D'abord n'oubliez pas de définir un getter et un setter dans l'entité propriétaire :
Code : 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
<?php
// Namespace\Bundle\Entity\Client

/**
 * @ORM\Entity
 */
class Client
{
    /**
     * @ORM\OneToOne(targetEntity="Namespace\Bundle\Entity\Adresse")
     */
    private $adresse;

    // On définit le getter et le setter associé.
    public function getAdresse()
    {
        return $this->adresse;
    }

    // Ici, on force le type de l'argument à être une instance de notre entité Adresse.
    public function setAdresse(\Namespace\Bundle\Entity\Adresse $adresse)
    {
        $this->adresse = $adresse;
    }
}

Pensez à bien forcer le type de l'argument pour le setter : cela permet de déclencher une erreur si vous essayez de passer un autre objet que Adresse à la méthode. Très utile pour éviter de chercher des heures l'origine d'un problème souvent bête.

Exemple d'utilisation


Pour utiliser cette relation, c'est très simple. Voici un exemple pour ajouter un nouveau Client et son Adresse depuis un contrôleur :
Code : 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
<?php
// Dans le contrôleur, pour ajouter un nouveau client et son adresse :
public function creerClientAction()
{
    $client = new Client;
    // $client->setNom() etc

    $adresse = new Adresse;
    // $adresse->setRue() etc

    // On lie l'adresse au client
    $client->setAdresse($adresse);

    // On récupère l'EntityManager
    $em = $this->getDoctrine()->getEntityManager();

    // On persiste les deux nouvelles entités
    $em->persist($client);
    $em->persist($adresse);

    // On déclenche l'enregistrement
    $em->flush();

    return new Response('OK');
}


Et voici un autre exemple, qui modifie un Client et son Adresse déjà existants :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// Dans le contrôleur toujours, mais pour modifier l'adresse déjà existante d'un client déjà existant :
public function modifierAdresseAction($id_client)
{
    $em = $this->getDoctrine()->getEntityManager();

    // On récupère le client.
    $client = $em->getRepository('SdzBlogBundle:Client')->find($id_client);

    // On modifie le numéro de l'adresse.
    $client->getAdresse()->setNumero(35);

    // On n'a pas besoin de persister notre client (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'adresse 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. Restons avec notre exemple des clients et leur adresse, et imaginons maintenant que notre application permette à nos clients d'enregistrer plusieurs adresses. Nous avons ainsi plusieurs adresses (Many) à lier (To) à un seul client (One).

Définition de la relation dans les entités


Annotation


Pour établir cette relation dans votre entité, la syntaxe est la suivante :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
/**
 * @ORM\Entity
 */
class Client
{
    // Nul besoin de rajouter de propriété, ici.

    // ...
}

/**
 * @ORM\Entity
 */
class Adresse
{
    /**
     * @ORM\ManyToOne(targetEntity="Namespace\Bundle\Entity\Client")
     */
    private $client;

    // ...
}

L'annotation à utiliser est ManyToOne, intuitif.

Première remarque : l'entité propriétaire a changé, c'est maintenant Adresse. Pourquoi ? Parce que rappelez-vous, le propriétaire est celui qui contient la colonne référence. Ici, on aura bien une colonne « client_id » dans la table « adresse ». 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 adresses pour un seul client, le Many correspond aux adresses, donc l'entité Adresse est la propriétaire.

Getter et setter


Ajoutons maintenant le getter et le setter correspondant dans l'entité propriétaire :
Code : 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
<?php
// Dans le fichier de l'entité.

/**
 * @ORM\Entity
 */
class Adresse
{
    /**
     * @ORM\ManyToOne(targetEntity="Namespace\Bundle\Entity\Client")
     */
    private $client;

    // On définit le getter et le setter associé.
    public function getClient()
    {
        return $this->client;
    }

    // Ici, on force le type de l'argument à être une instance de notre entité Client.
    public function setClient(\Namespace\Bundle\Entity\Client $client)
    {
        $this->client = $client;
    }
}


Exemple d'utilisation


La méthode pour gérer les multiples adresses est un peu différente, voyez dans ces exemples.

Tout d'abord pour ajouter un nouveau client et ses adresses :
Code : 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
// Dans le contrôleur, pour ajouter un nouveau client et ses adresses :

public function creerClientAction()
{
    $client = new Client;
    // $client->setNom() etc

    $adresse1 = new Adresse;
    // $adresse1->setRue() etc
    $adresse2 = new Adresse;
    // $adresse2->setRue() etc

    // On lie les adresses au client.
    $adresse1->setClient($client);
    $adresse2->setClient($client);

    // On récupère l'EntityManager.
    $em = $this->getDoctrine()->getEntityManager();

    // On persiste nos trois entités
    $em->persist($client);
    $em->persist($adresse1);
    $em->persist($adresse2);

    // On déclenche l'enregistrement.
    $em->flush();

    return new Response('OK');
}


Puis voici un moyen pour modifier une adresse déjà existante. Vous verrez qu'ici, ne pouvant utiliser $client->getAdresses(), on a utilisé le repository :

Code : 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
// Dans le contrôleur toujours, mais pour modifier une adresse déjà existante :

use Symfony\Component\Security\Core\Exception\AccessDeniedException;

public function modifierAdresseAction($id_adresse)
{
    $em = $this->getDoctrine()->getEntityManager();

    // On récupère l'adresse.
    $adresse = $em->getRepository('SdzBlogBundle:Adresse')->find($id_adresse);

    // On vérifie que l'adresse correspond au client en session (par exemple).
    if($adresse->getClient()->getId() != $this->get('session')->get('client_id'))
    {
        throw new AccessDeniedException("Vous n'avez pas le droit de modifier cette adresse.");
    }

    // On modifie le numéro de l'adresse.
    $adresse->setNumero(35);

    // On n'est pas obligé de persister notre $adresse car on l'a récupéré depuis Doctrine.

    // On ne passe pas du tout par le client ici en fait, donc pas besoin de persist non plus pour lui.

    // On déclenche la modification.
    $em->flush();

    return new Response('OK');
}

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 ! O.K., prenons l'exemple cette fois-ci des membres d'un site, divisés en groupes. Un membre peut appartenir à plusieurs groupes. À l'inverse, un groupe peut contenir plusieurs membres. On a donc une relation Many-To-Many entre Membre et Groupe.

Cette relation est particulière dans le sens où Doctrine va devoir créer une table intermédiaire. En effet, comment faites-vous pour faire ce genre de relation ? Vous avez une table « membre », une autre table « groupe », mais vous avez surtout besoin d'une table « membre_groupe » qui fait la liaison entre les deux ! Cette table de liaison ne contient que deux colonnes : « membre_id » et « groupe_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 !

Définition de la relation dans les entités


Annotation


Pour établir cette relation dans vos entités, la syntaxe est la suivante :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
/**
 * @ORM\Entity
 */
class Membre
{
    /**
     * @ORM\ManyToMany(targetEntity="Namespace\Bundle\Entity\Groupe")
     */
    private $groupes;

    // ...
}

/**
 * @ORM\Entity
 */
class Groupe
{
    // Nul besoin d'ajouter une propriété, ici.

    // ...
}

J'ai mis Membre 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 groupes d'un membre se fera assez souvent, alors que récupérer les membres d'un groupe, moins. Et puis, pour récupérer les membres d'un groupe, on aura surement besoin de personnaliser la requête, donc on le fera de toute façon depuis le GroupeRepository.

Getter et setter


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 une propriété (ici <?php $groupes) qui contient une liste d'objets. Il faut donc la définir comme un ArrayCollection dans le constructeur. Un ArrayCollection est un objet utilisé par Doctrine2, qui a toute 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 pratique, que nous verrons.

Ensuite, si le getter est classique, c'est le setter qui va différer un peu. En effet, "groupes" est un ensemble de groupes, mais au quotidien ce qu'on va faire c'est ajouter un groupe à cet ensemble. Il nous faut donc une méthode addGroupe() (sans "s", on n'ajoute qu'un seul groupe) et non une setGroupes().

Voici donc l'entité propriétaire prête à être utilisée :
Code : 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
31
32
33
34
<?php
// Dans le fichier de l'entité.

/**
 * @ORM\Entity
 */
class Membre
{
    /**
     * @ORM\ManyToMany(targetEntity="Namespace\Bundle\Entity\Groupe")
     */
    private $groupes;

    // Comme la propriété groupes doit être un ArrayCollection, souvenez-vous ;
    // on doit la définir dans un constructeur.
    public function __construct()
    {
        $this->groupes = new \Doctrine\Common\Collections\ArrayCollection;
    }

    // On est dans le côté propriétaire, on définit le getter.
    // Notez le « s » à getGroupes, on récupère une liste de groupes ici !
    public function getGroupes()
    {
        return $this->groupes;
    }

    // Le setter. Attention, on n'a pas un setGroupes() mais un addGroupe() sans « s » !
    public function addGroupe(\Namespace\Bundle\Entity\Groupe $groupe)
    {
        // On traite vraiment notre ArrayCollection comme un tableau.
        $this->groupes[] = $groupe;
    }
}


Exemples d'utilisation


Voici un exemple pour ajouter un nouveau membre à plusieurs groupes déjà existants :
Code : 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
31
32
33
<?php
// Dans le contrôleur, pour ajouter un nouveau membre à plusieurs groupes existants :

public function ajouterMembreAction()
{
    $membre = new Membre;
    // $membre->setPseudo() etc

    // On récupère l'EntityManager.
    $em = $this->getDoctrine()->getEntityManager();

    // On récupère les groupes par défaut à l'inscription d'un membre.
    // Cette méthode n'existe pas, il faudrait la créer bien sûr.
    $groupes = $em->getRepository('SdzBlogBundle:Groupe')->getGroupesParDefaut();

    // $groupes est ici un ArrayCollection, et voyez par la suite, on le traite comme un tableau normal.

    // Pour chaque groupe, on y ajoute le nouveau membre.
    foreach($groupes as $groupe)
    {
        $membre->addGroupe($groupe);
    }

    // On persiste juste le membre.
    // En effet, les groupes existent déjà et on ne les modifie pas, donc pas besoin de les persister.
    // Et la relation entre membre et groupe, c'est le membre qui la gère, donc persister le membre persistera la relation.
    $em->persist($membre);

    // On déclenche l'enregistrement.
    $em->flush();

    return new Response('OK');
}


Retenez surtout la boucle sur les groupes pour ajouter chaque groupe un à un au membre à modifier.

Enfin, voici un dernier exemple pour enlever un membre d'un groupe :
Code : 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
<?php
// Dans le contrôleur toujours, mais pour enlever d'un groupe un membre :

public function enleverGroupeAction($groupe)
{
    $em = $this->getDoctrine()->getEntityManager();

    // On récupère le membre, bien sûr on suppose ici que son id est stocké en session.
    $membre_id = $this->get('session')->get('membre_id');
    $membre = $em->getRepository('SdzBlogBundle:Membre')->find($membre_id);

    // On enlève le groupe.
    // Ici, on fait appel à la méthode removeElement() de l'ArrayCollection groupes.
    // Attention ici $groupe est bien une instance de Group, et pas seulement un id.
    $membre->getGroupes()->removeElement($groupe);

    // On a modifié la relation membre-groupe, or c'est le membre qui en est le propriétaire.
    // Donc on persiste le membre pour persister la relation.
    $em->persist($membre);

    // On déclenche la modification.
    $em->flush();

    return new Response('OK');
}

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 Commande_Produit. 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 Commande_Produit Many-To-One Produit

En effet, une commande (One) peut avoir plusieurs relations avec des produits (Many), plusieurs Commande_Produit donc ! La relation est symétrique pour les produits.

Attention, dans le titre de cette sous-partie 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 :) .

Définition de la relation dans les entités


Annotation


Il faut donc créer notre entité de relation (notre Commande_Produit) comme ceci :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

/**
 * @ORM\Entity
 */
class Commande_Produit
{
    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Namespace\Bundle\Entity\Commande")
     */
    private $commande;

    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Namespace\Bundle\Entity\Produit")
     */
    private $produit;

    // Et les autres attributs comme la quantite, etc.

    // ...
}

Comme les côtés Many des deux relations Many-To-One sont dans Commande_Produit, cette entité est l'entité propriétaire des deux relations.

Et ces @ORM\Id ? Pourquoi il y en a 2 et qu'est-ce qu'ils viennent faire ici ?

Très bonne question. Comme toute entité, notre Commande_Produit 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 rajouté un attribut id qui était en auto-incrément, on ne s'en occupait pas trop donc. Ici c'est différent, comme une Commande_Produit correspond à une unique couple Commande/Produit (pour chaque couple Commande/Produit, il n'y a qu'une seule Commande_Produit), 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 uni-directionnelle, on ne pourra pas faire $commande->getCommandeProduits() pour récupérer les Commande_Produit et donc les produits ? ? Ni l'inverse depuis $produit ?

En effet, et c'est pourquoi la prochaine sous-partie de ce chapitre traite des relations bidirectionnelles ! En attendant, pour notre relation OneToManyToOne, continuons simplement sur une relation unidirectionnelle.

Sachez quand même que vous pouvez éviter une relation bi-directionnelle ici en utilisant simplement la méthode findByCommande($commande) (pour récupérer les produits d'une commande) ou findByProduit($produit) (pour l'inverse) du repository CommandeProduitRepository.

L'intérêt de la bi-directionnelle ici est lorsque vous voulez afficher une liste des commandes avec leurs produits. Dans la boucles 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 faut passer par un $commande->getCommandeProduits().


Getter et setter


Rien de transcendant sur les getter et setter, ce sont ceux d'une simple relation Many-To-One, que voici :
Code : 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
31
32
33
34
35
36
37
38
<?php
/**
 * @ORM\Entity
 */
class Commande_Produit
{
    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Namespace\Bundle\Entity\Commande")
     */
    private $commande;

    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Namespace\Bundle\Entity\Produit")
     */
    private $produit;

    // On définit le getter et le setter pour Commande
    public function getCommande()
    {
        return $this->commande;
    }
    public function setCommande(\Namespace\Bundle\Entity\Commande $commande)
    {
        $this->commande = $commande;
    }

    // On définit le getter et le setter pour Produit
    public function getProduit()
    {
        return $this->produit;
    }
    public function setProduit(\Namespace\Bundle\Entity\Produit $produit)
    {
        $this->produit = $produit;
    }
}

Les annotations et les getter/setter sont du côté propriétaire, donc le côté Many, donc tout va bien.

Exemple d'utilisation


La manipulation des entités dans une telle relation est un peu plus compliqué, surtout sans la bidirectionnalité. Mais on peut tout de même s'en sortir. Tout d'abord, pour créer une nouvelle Commande contenant plusieurs produits :
Code : 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
// Dans le contrôleur

public function creerCommandeAction()
{
    $em = $this->getDoctrine()
               ->getEntityManager();

    // Les produits existent déjà, on les récupère depuis la bdd
    $liste_produits = $em->getRepository('SdzBlogBundle:Produit')
                         ->findAll(); // Pour l'exemple, notre Commande contient tous les Produit

    // On crée la Commande
    $commande = new Commande;

    // On attribue les produits à la commande
    $i = 0;
    foreach($liste_produits as $produit)
    {
        // On crée une nouvelle "relation entre 1 produit et 1 commande"
        $commandeProduit[$i] = new Commande_Produit;
        // On définit la commande, qui est ici toujours la même
        $commandeProduit[$i]->setCommande($commande);
        // On définit le produit, qui change ici dans la boucle foreach
        $commandeProduit[$i]->setProduit($produit);
        // Arbitrairement, on dit que chaque produit est commandé en 3 exemplaires
        $commandeProduit[$i]->setQuantite(3);

        // Et bien sûr, on persiste cette relation
        $em->persist($commandeProduit[$i]);

        $i++; // On incrémente le $i qui ne sert qu'à indexer le tableau $commandeProduit
    }

    // On n'oublie pas de persister la commande
    $em->persist($commande);

    // Pas besoin de persister les produits : ils n'ont pas changé et, de plus,
    // ils sont déjà persistés car on les a récupéré depuis Doctrine

    // On déclenche l'enregistrement.
    $em->flush();

    return new Response('OK');
}

Et un exemple pour récupérer les articles et leurs quantité d'une certaine commande.

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// Dans le contrôleur :

public function voirCommandeAction($id_commande)
{
    $em = $this->getDoctrine()
               ->getEntityManager();

    // On récupère la commande
    $commande = $em->getRepository('SdzBlogBundle:Commande')
                   ->find($id_commande);

    // On récupère les commandeProduits pour la commande $commande
    $commandeProduits = $em->getRepository('SdzBlogBundle:Commande_Produit')
                           ->findByCommande($commande);

    // On affiche la vue Twig :
    return $this->render('SdzBlogBundle:TestCommande:commande.html.twig', array(
        'commandeProduits' => $commandeProduits
        'commande'         => $commande)
    );
}

Et le code de la vue correspondante :
Code : HTML
1
2
3
4
5
6
Liste des produits de la commande {{ commande.nom }} :
<ul>
  {% for commandeProduit in commandeProduits %}
    <li>{{ commandeProduit.quantite }} exemplaire(s) du produit {{ commandeProduit.produit.nom }} ;</li>
  {% endfor %}
</ul>

C'est un exemple simple bien sûr, dans lequel j'ai considéré que les entités Commande et Produit avaient un attribut Nom pour les afficher.

Dans cet exemple, la méthode findByCommande() utilisée dans le contrôleur ne sélectionne que les Commande_Produit. Donc, lorsque dans la boucle dans la vue, on fait {{ commandeProduit.produit }}, en réalité Doctrine va effectuer une requête pour récupérer le Produit associé à cette Commande_Produit. C'est bien sûr une horreur, car il va faire une requête... par itération dans le for ! Si vous avez 20 produits dans la commande, cela ferait 20 requêtes, inimaginable.

Pour charger les Produits en même temps que les Commande_Produit 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 Commande_Produit. On voit tout ça dans le chapitre suivant dédié aux repository. 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 findByCommande, 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()). 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 ! A 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 sous partie. Sachez que la documentation l'explique également très bien : vous pourrez vous renseigner sur le chapitre sur les associations, puis celui sur l'utilisation.

Définition de la relation dans les entités


Pour étudier la définition d'une relation bidirectionnelle, nous allons étudier une relation ManyToOne. Souvenez-vous bien de cette relation, dans sa version unidirectionnelle, pour pouvoir attaquer sa version bidirectionnelle dans les meilleures conditions.

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 ne rajoute pas d'attribut dans l'entité inverse. La première étape consiste donc à rajouter une annotation à notre entité inverse Client :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
/**
 * @ORM\Entity
 */
class Client
{
    /**
     * @ORM\OneToMany(targetEntity="Namespace\Bundle\Entity\Adresse", mappedBy="client")
     */
    private $adresses; // Ici adresses prend un "s", car on a plusieurs adresses pour un client.

    // ...
}

Bien entendu, je vous dois des explications sur ce que l'on vient de faire.

Commençons par l'annotation. L'inverse d'un ManyToOne est... un OneToMany, tout simplement ! Il faut donc utiliser l'annotation OneToMany dans l'entité inverse. Je rappelle que le propriétaire d'une relation ManyToOne est toujours le côté Many, donc lorsque vous voyez l'annotation ManyToOne, vous êtes forcément du côté propriétaire.

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 Adresse. Le mappedBy correspond lui à l'attribut de l'entité propriétaire (Adresse) qui pointe vers l'entité inverse (Client) : le "private $client" de l'entité Adresse. 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 en rajoutant le paramètre inversedBy dans l'annotation ManyToOne :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php

/**
 * @ORM\Entity
 */
class Adresse
{
    /**
     * @ORM\ManyToOne(targetEntity="Namespace\Bundle\Entity\Client", inversedBy="adresses")
     */
    private $client;

    // ...
}

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 (Client) qui point vers l'entité propriétaire (Adresse). C'est donc l'attributs "adresses".

Tout est bon côté annotation, maintenant il faut également rajouter les getter et setter dans l'entité inverse bien entendu.

Getter et setter


Bien entendu, on part d'une relation unidirectionnelle fonctionnelle, donc les getter et setter de l'entité propriétaire sont bien définis.

Dans un premier temps, rajoutons assez logiquement le getter et le setter dans l'entité inverse. On vient de lui rajouter un attribut, il est normal que le getter et le setter aillent de paire. Comme nous sommes du côté One d'un OneToMany, l'attribut "adresses" est un ArrayCollection. C'est donc un addAdresse / getAdresses qu'il nous faut :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
/**
 * @ORM\Entity
 */
class Client
{
    /**
     * @ORM\OneToMany(targetEntity="Namespace\Bundle\Entity\Adresse", mappedBy="client")
     */
    private $adresses;

    public function getAdresses()
    {
        return $this->adresses;
    }

    public function addAdresse(\Namespace\Bundle\Entity\Adresse $adresse)
    {
        $this->adresses[] = $adresse;
    }

    // ...
}


Maintenant, il faut nous rendre compte d'un petit manque. Voici une petite problématique, lisez bien ce code :

Code : PHP
1
2
3
4
5
6
7
<?php
// Création des entités
$client = new Client;
$adresse = new Adresse;

// On lie l'adresse au client
$client->addAdresse($adresse);

Question : Que retourne $adresse->getClient() ?

Réponse : Rien ! En effet pour qu'un $adresse->getClient() retourne un client, il faut d'abord le lui définir en appelant $adresse->setClient(), c'est logique !

C'est logique en soi, mais du coup dans notre code cela va être moins beau : il faut en effet lier l'adresse au client, et le client à l'adresse. Comme ceci :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
// Création des entités
$client = new Client;
$adresse = new Adresse;

// On lie l'adresse au client
$client->addAdresse($adresse);

// On lie le client à l'adresse
$adresse->setClient($client);

Mais ces deux méthodes étant intimement liées, on doit en fait les imbriquer. En effet, laisser le code dans l'état est possible, mais imaginez qu'un jour vous oubliez d'appeler l'une des deux méthodes, et 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 :

Code : 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
<?php
/**
 * @ORM\Entity
 */
class Client
{
    /**
     * @ORM\OneToMany(targetEntity="Namespace\Bundle\Entity\Adresse", mappedBy="client")
     */
    private $adresses;

    public function getAdresses()
    {
        return $this->adresses;
    }

    public function addAdresse(\Namespace\Bundle\Entity\Adresse $adresse)
    {
        $this->adresses[] = $adresse;
        $adresse->setClient($this);
    }

    // ...
}

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 addAdresse() exécute setClient(), qui exécute à son tour addAdresse(), qui... etc. On se retrouve avec une boucle infinie, pas joli. Bref, l'important est de se prendre un côté (propriétaire ou inverse 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 ici exécuter $client->addAdresse() qui garde la cohérence entre les deux entités. Il ne faudra pas exécuter $adresse->setClient(), 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 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'entrainement, et à voir leur comportement dans les relations.
Chapitre précédent Sommaire Chapitre suivant

Partager

16 commentaires pour "Les relations entre entités avec Doctrine2"
Note moyenne : 3.75 / 4 (245 votes)
Pseudo Commentaire
Hors ligne park # Posté le 13/04/2012 à 20:24:11
Avatar

Avis : Très bon

Suuuuuupeeeeeeeeer ce tuto :D
Hors ligne antho.bg87 # Posté le 14/05/2012 à 04:16:14
Avatar

Salut !

J'ai une question concernant la partie ou on entre les entity. Winzou tu dis qu'il ne faut pas creer une colonne article_id dans la table des commentaires mais dans la doc pour la relation One-to-Many bidirectionelle ils font :

Code : XML
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<doctrine-mapping>
    <entity name="Product">
        <one-to-many field="features" target-entity="Feature" mapped-by="product" />
    </entity>
    <entity name="Feature">
        <many-to-one field="product" target-entity="Product" inversed-by="features">
            <join-column name="product_id" referenced-column-name="id" />
        </many-to-one>
    </entity>
</doctrine-mapping>


Si je comprend bien dans Feature ils ont un $product et dans Product un $features et le product_id c'est une colonne qui va se creer ensuite pour servir de foreign key dans la DB ? Cette colonne il ne faut pas l'entrer lors de la generation de l'entity ?

Merci et chapeau pour le tuto !! :)


PS: Dsl pour le commentaire, j'ai poser ma question sur le forum ;)
Hors ligne winzou # Posté le 14/05/2012 à 19:17:40
lala
Avatar

Avis : Très bon Modérateurs

Ville : Singapour
Pays : Singapour
Études : Ecole Centrale de Lyon

Salut antho.bg87,

Il y a le OneToMany d'un côté et le ManyToOne de l'autre : c'est une relation bidirectionnelle. Je l'explique à la fin du chapitre.

Concernant la balise "join-column" en elle même, elle ne sert à rien ici. Par défaut, Doctrine2 va créer tout seul une colonne "product_id". Le fait de rajouter cette balise permet de personnaliser ce nom, mais c'est la plupart du temps inutile.

Un tutoriel pour débuter avec le framework Symfony2.
Chapitre en beta-test : Déployer son site Symfony2 en production, donnez vos avis !

Je recherche toujours quelqu'un capable de faire des icônes sympas pour les chapitres du tutoriel, contactez-moi, merci !
 
Hors ligne akrramo # Posté le 16/05/2012 à 21:29:42

Salut winzou,
j'ai une petite question concernant l'identifiant de l'entité Commande_Produit. sur le tutorial , tu as exploité les identifiants des entités associés pour avoir un identifiant unique.
J'ai utilisé :

Code : Console
php app/console generate:doctrine:entity


pour créer l'entité d'association, donc celà crée automatiquement un champs @id
je voudrais savoir si c'est possible d'utiliser que ce champs @id au lieu d'utiliser les deux identifiants. et si c'est la même chose?

the future belongs to those who prepare for it today
:lol:
 
Hors ligne winzou # Posté le 17/05/2012 à 11:13:01
lala
Avatar

Avis : Très bon Modérateurs

Ville : Singapour
Pays : Singapour
Études : Ecole Centrale de Lyon

Salut,

Oui bien sûr c'est possible. C'est juste que ça te crée un attribut (id) inutile dans ton objet Commande_Produit.

Un tutoriel pour débuter avec le framework Symfony2.
Chapitre en beta-test : Déployer son site Symfony2 en production, donnez vos avis !

Je recherche toujours quelqu'un capable de faire des icônes sympas pour les chapitres du tutoriel, contactez-moi, merci !
 

Voir tous les commentaires