Relation ManyToMany des entités Doctrine

Logo DoctrineSuite du tutoriel sur la création des relations entre entités pour leur persistance en base de données avec Doctrine.

Pour les précédent tutoriels :

Dans cette partie nous traiterons de la relation ManyToMany.

relation ManyToMany

Un Client a acheté zéro ou plusieurs Produits.
Un Produit a été acheté par zéro ou plusieurs Clients.
Oui, dans ce modèle ci le client ne peut acheter qu’une fois un produit…

Voici le SQL correspondant :

TABLE `Client` (
  `idClient` INT NOT NULL ,
  `nom` VARCHAR(45) NULL ,
  PRIMARY KEY (`idClient`) ) ;

TABLE `Produit` (
  `idProduit` INT NOT NULL ,
  `nom` VARCHAR(45) NULL ,
  PRIMARY KEY (`idProduit`) ) ;

TABLE `Acheter` (
  `Client_idClient` INT NOT NULL ,
  `Produit_idProduit` INT NOT NULL ,
  PRIMARY KEY (`Client_idClient`, `Produit_idProduit`) ,
  INDEX `fk_Acheter_Produit1` (`Produit_idProduit` ASC) ,
  CONSTRAINT `fk_Acheter_Client`
    FOREIGN KEY (`Client_idClient` )
    REFERENCES `mydb`.`Client` (`idClient` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_Acheter_Produit1`
    FOREIGN KEY (`Produit_idProduit` )
    REFERENCES `mydb`.`Produit` (`idProduit` )
) ;

Trois tables mais attention uniquement deux entités. La table Acheter ne donne pas lieu à la création d’une entité. Elle est le résultat de la liaison multiple entre les deux entités.

namespace WaldoRelationBundleEntity;

use DoctrineORMMapping as ORM;
use DoctrineCommonCollectionsArrayCollection;

/**
 * @ORMTable(name="Client")
 * @ORMEntity
 */
class Client
{

    /**
     * @ORMColumn(name="idClient", type="bigint", nullable=false)
     * @ORMId
     * @ORMGeneratedValue(strategy="IDENTITY")
     */
    private $identifiantClient;

    /**
     * @ORMColumn(name="nom", type="string", length=45, nullable=true)
     */
    private $nom;

    /**
     * @var ArrayCollection Produit $produits
     * Owning Side
     *
     * @ORMManyToMany(targetEntity="Produit", inversedBy="clients", cascade={"persist", "merge"})
     * @ORMJoinTable(name="Acheter",
     *   joinColumns={@ORMJoinColumn(name="Client_idClient", referencedColumnName="idClient")},
     *   inverseJoinColumns={@ORMJoinColumn(name="Produit_idProduit", referencedColumnName="idProduit")}
     * )
     */
    private $produits;

    /**
     * @return int
     */
    public function getIdentifiantClient()
    {
        return $this->identifiantClient;
    }

    /**
     * @param string $nom
     */
    public function setNom($nom)
    {
        $this->nom = $nom;
    }

    /**
     * @return string
     */
    public function getNom()
    {
        return $this->nom;
    }

    /**
     * Add Produit
     *
     * @param Produit $produit
     */
    public function addProduit(Produit $produit)
    {
        // Si l'objet fait déjà partie de la collection on ne l'ajoute pas
        if (!$this->produits->contains($produit)) {
            $this->produits->add($produit);
        }
    }

    public function setProduits($items)
    {
        if ($items instanceof ArrayCollection || is_array($items)) {
            foreach ($items as $item) {
                $this->addProduit($item);
            }
        } elseif ($items instanceof Produit) {
            $this->addProduit($items);
        } else {
            throw new Exception("$items must be an instance of Produit or ArrayCollection");
        }
    }

    /**
     * Get ArrayCollection
     *
     * @return ArrayCollection $produits
     */
    public function getProduits()
    {
        return $this->produits;
    }

    public function __construct()
    {
        $this->produits = new ArrayCollection();
    }
}
namespace WaldoRelationBundleEntity;

use DoctrineORMMapping as ORM;
use DoctrineCommonCollectionsArrayCollection;

/**
 * @ORMTable(name="Produit")
 * @ORMEntity
 */
class Produit
{

    /**
     * @ORMColumn(name="idProduit", type="bigint", nullable=false)
     * @ORMId
     * @ORMGeneratedValue(strategy="IDENTITY")
     */
    private $identifiantProduit;

    /**
     * @ORMColumn(name="nom", type="string", length=45, nullable=true)
     */
    private $nom;

    /**
     * @var ArrayCollection Produit $clients
     *
     * Inverse Side
     *
     * @ORMManyToMany(targetEntity="Client", mappedBy="produits", cascade={"persist", "merge"})
     */
    private $clients;

    /**
     * @return int
     */
    public function getIdentifiantProduit()
    {
        return $this->identifiantProduit;
    }

    /**
     * @param string $nom
     */
    public function setNom($nom)
    {
        $this->nom = $nom;
    }

    /**
     * @return string
     */
    public function getNom()
    {
        return $this->nom;
    }

    /**
     * Add Client
     *
     * @param Client $client
     */
    public function addClient(Client $client)
    {
        // Si l'objet fait déjà partie de la collection on ne l'ajoute pas
        if (!$this->clients->contains($client)) {
            if (!$client->getProduits()->contains($this)) {
                $client->addProduit($this);  // Lie le Client au produit.
            }
            $this->clients->add($client);
        }
    }

    public function setClients($items)
    {
        if ($items instanceof ArrayCollection || is_array($items)) {
            foreach ($items as $item) {
                $this->addClient($item);
            }
        } elseif ($items instanceof Client) {
            $this->addClient($items);
        } else {
            throw new Exception("$items must be an instance of Client or ArrayCollection");
        }
    }

    /**
     * Get ArrayCollection
     *
     * @return ArrayCollection $clients
     */
    public function getClients()
    {
        return $this->clients;
    }

    public function __construct()
    {
        $this->clients = new ArrayCollection();
    }

}

Avec ce code on pourra faire ce genre de chose :

$client1 = new Client();
$client1->setNom("client1");
$client2 = new Client();
$client2->setNom("client2");

$produit1 = new Produit();
$produit1->setNom("produit1");
$produit2 = new Produit();
$produit2->setNom("produit2");

$produit1->setClients(array($client1, $client2));
$client1->addProduit($produit1);
$client1->setProduits($produit2);

$this->getDoctrine()->getEntityManager()->persist($produit1);
$this->getDoctrine()->getEntityManager()->flush();

Avec le code ci-dessus la base de données contiendra deux clients, deux produits, et trois liaisons dans la table Acheter.

Explication :
Dans cet exemple, l’entité Client a été choisie pour être la partie propriétaire (Owning Side).
Ceci nécessite un éclaircissement : pour faire simple, dans une relation ManyToMany avec Doctrine vous devez définir une propriété comme « Propriétaire », dans notre cas $produits dans l’entité Client. Dans l’entité visée une propriété dite « Inverse » doit être défini, ici $clients dans l’entité Produit. C’est ce jeu d’annotation qui permet à Doctrine de faire le lien entre les deux entités.

Ce système implique que pour persister la relation entre un client et un produit, il faut persister le client auquel a été ajouté un produit . Si vous persistez le produit, dans la base on retrouvera bien un client et un produit mais aucune relation.
Pas de panique, il est possible de palier à cette limitation et je vous l’explique un peu plus loin. Sinon pour en savoir plus sur Owning Side et Inverse Side : http://www.doctrine-project.org/docs/orm/2.0/en/reference/association-mapping.html

/**
 * @var ArrayCollection Produit $produits
 * Owning Side
 *
 * @ORMManyToMany(targetEntity="Produit", inversedBy="clients", cascade={"persist", "merge"})
 * @ORMJoinTable(name="Acheter",
 *   joinColumns={@ORMJoinColumn(name="Client_idClient", referencedColumnName="idClient")},
 *   inverseJoinColumns={@ORMJoinColumn(name="Produit_idProduit", referencedColumnName="idProduit")}
 * )
*/
private $produits;

L’annotation ManyToMany définie que la propriété $produits peut contenir un ou plusieurs objets. $produits est de type ArrayCollection.
L’attribut targetEntity détermine de quelle classe dépendent les objets que la propriété $produits peut contenir.
L’attribut inversedBy détermine le nom de la propriété présente dans la classe visé (Produit) qui sert de lien.
cascade={"persist", "merge"}) : permet de définir le comportement lors de la persistance de l’objet Client.
L’annotation JoinTable permet de définir la manière dont sont liées les deux entités Client et Produit.
L’attribut name correspond au nom de la table.
L’annotation joinColumns permet de définir la liaison « propriétaire » entre la table Client et Acheter.
L’annotation inverseJoinColumns permet de définir la liaison inverse entre la table Produit et Acheter.

L’annotation JoinColumn permet de déterminer quel champ de la base de données permet la relation.
L’attribut name correspond au champs de la table Departement qui permet la relation.
L’attribut referencedColumnName est le nom du champs de la table visé qui permet la relation.

Dans cet exemple la relation ManyToMany est des plus simple. Pour les structures plus complexe il est possible de définir plusieurs annotations JoinColumn dans joinColumns et inverseJoinColumns.

/**
 * @var ArrayCollection Produit $clients
 *
 * Inverse Side
 *
 * @ORMManyToMany(targetEntity="Client", mappedBy="produits", cascade={"persist", "merge"})
 */
 private $clients;

L’annotation ManyToMany définie que la propriété $clients peut contenir un ou plusieurs objets. D’ailleurs $clients est de type ArrayCollection.
L’attribut targetEntity définit la classe visée.
L’attribut mappedBy détermine la propriété inverse de cette liaison
cascade={"persist", "merge"}) : permet de définir le comportement lors de la persistance de l’objet Client.

Ici pas besoin de redéfinir les liaisons avec la table Acheter, doctrine les connaits.

La manière de gérer les relations ManyToMany de Doctrine peu paraître un peu restrictive, c’est pourquoi dans l’entité qui gère l’« Inverse », j’ai ajouté ceci.

    /**
     * Add Client
     *
     * @param Client $client
     */
    public function addClient(Client $client)
    {
        // Si l'objet fait déjà partie de la collection on ne l'ajoute pas
        if (!$this->clients->contains($client)) {
            if (!$client->getProduits()->contains($this)) {
                $client->setProduits($this);  // Lie le Client au produit.
            }
            $this->clients->add($client);
        }
    }

Le code « $client->setProduits($this); » est particulièrement intéressant car il permet de renforcer le lien entre le produit et le clients.
Ce qui nous offre la possibilité de persister soit l’objet client soit l’objet produit sans s’inquiéter de la création des relations dans la base de données.

Publié dans Doctrine, PHP, Symfony2
3 commentaires pour “Relation ManyToMany des entités Doctrine
  1. Lorine dit :

    Merci pour ce tutoriel ça aide beaucoup.
    Par contre une question :
    Comment fait-on si on veux ajouter le champ « date » dans « acheter »? un exemple?
    Merci

    • waldo2188 dit :

      @Lorine, Désolé pour le retard dans la réponse…
      Donc pour ajouter un champs « date » à ta relation ternaire, il te faut créer un troisième objet « Acheter » et le relier au objet « Client » et « Produit » avec des relations OneToMany et ManyToOne.

      En espérant ne pas t’avoir répondus trop tard…

  2. Lorine dit :

    Merci pour la réponse, j’avais finalement trouvé.
    Par contre, une fois tout cela mis en place comment faire pour le getClients? lorsque je veux récupérer ce qu’il contient, j’ai l’erreur « Undefined index: joinTable in …DoctrineOrmDoctrineORMMappingDefaultQuoteStrategy.php on line 90 »

1 Pings/Trackbacks pour "Relation ManyToMany des entités Doctrine"
  1. […] Relation ManyToMany des entités Doctrine […]

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*