Les relations entre entités dans Symfony2 avec Doctrine

Nous savons comment créer des entités indépendantes, voyons maintenant dans ce chapitre comment lier nos différents objets avec Doctrine.




Création de l'entité DeskComment de notre projet


Afin d'illustrer les relations Doctrine dans Symfony2, nous allons commencer par créer une nouvelle entité DeskComment.

Comme vu précédemment, générez l'entité à l'aide de la commande Symfony2 prévue à cet effet.

Generation de DeskComment


Une fois que c'est fait, vous devriez obtenir la classe DeskComment suivante et sa classe repository qui va avec.
Ce qui donne cette classe une fois qu'on a ajouté le constructeur.
Code php:
<?php

namespace Wmd\WatchMyDeskBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="Wmd\WatchMyDeskBundle\Entity\DeskCommentRepository")
 * @ORM\Table(name="desk_comment")
 */
class DeskComment
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(name="created_at", type="datetime")
     */
    protected $createdAt;

    /**
     * @ORM\Column(type="text")
     */
    protected $description;

    /**
     * @ORM\Column(name="submission_ip", type="string", length="32")
     */
    protected $submissionIp;

    public function __construct()
    {
        $this->createdAt = new \DateTime('now');
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set createdAt
     *
     * @param datetime $createdAt
     */
    public function setCreatedAt($createdAt)
    {
        $this->createdAt = $createdAt;
    }

    /**
     * Get createdAt
     *
     * @return datetime 
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    /**
     * Set description
     *
     * @param text $description
     */
    public function setDescription($description)
    {
        $this->description = $description;
    }

    /**
     * Get description
     *
     * @return text 
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Set submissionIp
     *
     * @param string $submissionIp
     */
    public function setSubmissionIp($submissionIp)
    {
        $this->submissionIp = $submissionIp;
    }

    /**
     * Get submissionIp
     *
     * @return string 
     */
    public function getSubmissionIp()
    {
        return $this->submissionIp;
    }

}


N'oubliez pas de changer le nom des champs pour les passer en underscore case et changer le nom de la table en name="desk_comment".

Pendant que nous sommes sur les annotations, j'en profite pour vous informer d'un bug qui se glissera forcément plus d'une fois pendant vos débuts avec le framework.
Si vous mettez des ' à la place de " dans les annotations, le framework crash! Et l'erreur associée n'est pas forcément explicite. Donc retenez la bien pour la sauvegarde de vos cheveux !



Symfony2 erreur avec les annotations à cause des guillemets simples



Les relations OneToMany avec Doctrine dans Symfony2


Vous aurez compris que notre entité DeskComment va être liée à Desk par une relation OneToMany:
Un commentaire ne pourra être lié qu'à un seul bureau et un bureau pourra disposer de plusieurs commentaires.

Malheureusement, il n'existe pas de commande toute prête (Qui se lance ?) pour générer automatiquement la relation entre deux entités, il faut le faire à la main. Mais rassurez vous, rien de bien compliqué !

Commençons par ajouter la relation du côté de Desk.php.
Nous allons ajouter une propriété $comments dans la classe Desk et créer la relation grâce aux annotations:
Code php:
    /**
     * @ORM\OneToMany(targetEntity="DeskComment", mappedBy="desk", cascade={"remove", "persist"})
     */
    protected $comments;


  • targetEntity: Nom de l'entité à lier
  • mappedBy: Nom de la propriété qui contient l'objet Desk, ici ça sera $desk.
  • cascade: Comment doit réagir la relation


Pour les notions de cascade, référez vous à la documentation Doctrine2.

La 2ème étape consiste à lier l'autre entité: DeskComment.php
Ajoutons la propriété $desk comme nous l'avons spécifié dans le mappedBy de Desk.php:

Code php:
    /**
     * @ORM\ManyToOne(targetEntity="Desk", inversedBy="comments", cascade={"remove"})
     * @ORM\JoinColumn(name="desk_id", referencedColumnName="id")
     */
    protected $desk;


L'annotation est légèrement différente car on va spécifier dans la table desk_comment le champ desk_id qui permettra de faire la liaison entre les deux tables.

Tout le monde suit ?

Ok, nous avons créé nos deux propriétés dans les deux entités à lier, mais nous n'avons pas encore créé les accesseurs / mutateurs (get & set) de ces propriétés, indispensables pour le fonctionnement de nos objets.
En bon fainéant qui se respecte, nous allons utiliser la ligne de commande mise à disposition pour les générer:
php app/console doctrine:generate:entities WmdWatchMyDeskBundle


Symfony2 generate entities


Cette commande va directement ajouter les get et set de nos nouvelles propriétés dans les fichiers Desk.php et DeskComment.php.

Par sécurité, la commande crée un backup des fichiers Desk.php~ et DeskComment.php~, ne vous étonnez donc pas de leur présence. Vous pouvez les supprimer une fois avoir vérifié que la génération s'est bien passée.



Quittez et rouvrez les fichiers, vous devriez avoir les méthodes suivantes en bas de fichier Desk.php:
Code php:
    /**
     * Add comments
     *
     * @param \Wmd\WatchMyDeskBundle\Entity\DeskComment $comments
     */
    public function addDeskComment(\Wmd\WatchMyDeskBundle\Entity\DeskComment $comments)
    {
        $this->comments[] = $comments;
    }

    /**
     * Get comments
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getComments()
    {
        return $this->comments;
    }


Et dans DeskComment.php:
Code php:
    /**
     * Set desk
     *
     * @param \Wmd\WatchMyDeskBundle\Entity\Desk $desk
     */
    public function setDesk(\Wmd\WatchMyDeskBundle\Entity\Desk $desk)
    {
        $this->desk = $desk;
    }

    /**
     * Get desk
     *
     * @return \Wmd\WatchMyDeskBundle\Entity\Desk 
     */
    public function getDesk()
    {
        return $this->desk;
    }


Ajoutez la méthode setDeskComment dans Desk.php:

Code php:
    /**
     * Set comments
     *
     * @param \Doctrine\Common\Collections\Collection $comments
     */
    public function setDeskComment(\Doctrine\Common\Collections\Collection $comments)
    {
        $this->comments = $comments;
    }


Vous pouvez aussi modifier le contrôleur pour initialiser la collection des commentaires du bureau, ajoutez dans le __construct de Desk:

Code php:
    public function __construct()
    {
        $this->voteCount = 0;
        $this->createdAt = new DateTime('now');
        $this->isEnabled = false;
        
        $this->comments = new \Doctrine\Common\Collections\ArrayCollection();
    }


Notre relation est prête, il ne nous reste plus qu'à re-générer notre base de données.
Lancez la commande:

php app/console doctrine:schema:update --dump-sql


Code sql:
CREATE TABLE desk_comment (id INT AUTO_INCREMENT NOT NULL, desk_id INT DEFAULT NULL, description LONGTEXT NOT NULL, submission_ip VARCHAR(32) NOT NULL, created_at DATETIME NOT NULL, INDEX IDX_3754E6F871F9DF5E (desk_id), PRIMARY KEY(id)) ENGINE = InnoDB;
ALTER TABLE desk_comment ADD CONSTRAINT FK_3754E6F871F9DF5E FOREIGN KEY (desk_id) REFERENCES desk(id)


Notre table desk_comment va bien être créée et il en est de même pour sa foreign key avec Desk. On valide la mise à jour:
php app/console doctrine:schema:update --force




Manipuler les relations entre entités Doctrine dans Symfony2


Voyons à présent comment créer des objets et les mettre en relation pour les sauvegarder.

Ouvrez le contrôleur DefaultController.php et modifions notre action test ainsi:

Code php:
    public function testAction()
    {
        
        $em = $this->getDoctrine()->getEntityManager();
        $id = 1; // ID du bureau de test que l'on a enregistré précédemment
        
        $desk = $this->getDoctrine()->getRepository('WmdWatchMyDeskBundle:Desk')->find($id);
        echo "Le bureau récupéré porte l'ID: ".$desk->getId()." et le titre: ".$desk->getTitle();
        
        $comment = new DeskComment();
        $comment->setDescription("Mon premier commentaire: Joli bureau !");
        $comment->setSubmissionIp($this->getRequest()->server->get('REMOTE_ADDR'));
        $comment->setDesk($desk); // On lie le commentaire à notre bureau d'ID 1
        
        $em->persist($comment); // On persist le commentaire 1
        
        $comment2 = new DeskComment();
        $comment2->setDescription("Mon deuxième commentaire: J'adore le bureau ! Bravo !");
        $comment2->setSubmissionIp($this->getRequest()->server->get('REMOTE_ADDR'));
        $comment2->setDesk($desk); // On lie le commentaire à notre bureau d'ID 1
        
        $em->persist($comment2); // On persist le commentaire 2
        
        $em->flush(); // On sauvegarde en BDD les deux commentaires
        
        exit;
    }


Ouvrez la page /test/ dans votre navigateur et si tout s'est bien passé, vous devriez avoir 2 commentaires dans votre BDD avec pour desk_id: 1.

Vous avez eu une erreur ? Celle-ci je présume: Fatal error: Class 'WmdWatchMyDeskBundleControllerDeskComment' not found in XXX/wmd/src/Wmd/WatchMyDeskBundle/Controller/DefaultController.php on line 33. Vous n'avez pas encore eu le réflexe de rajouter "use Wmd\WatchMyDeskBundle\Entity\DeskComment;" dans votre DefaultController. Ça viendra !



Nous pouvons aller un peu plus loin et créer facilement l'affichage de notre bureau et de ses deux commentaires dans un template Twig.

Modifiez testAction ainsi:
Code php:
    /**
     * @Route("/test/", name="test")
     * @Template()
     */
    public function testAction()
    {
        $id = 1; // ID du bureau de test que l'on a enregistré précédemment
        
        $desk = $this->getDoctrine()->getRepository('WmdWatchMyDeskBundle:Desk')->find($id);
        
        return array('desk' => $desk);
    }


Puis créez le fichier Resources/views/Default/test.html.twig:
Code html:
<h1>{{ desk.title }}</h1>
<p>
    {{ desk.description }}
</p>
<h3>Commentaires</h3>
    {% for com in desk.comments %}
  • Commentaire du {{ com.createdAt|date('d/m/Y') }}

    <p> {{ com.description }} </p>
  • {% else %}
  • Aucun commentaire
  • {% endfor %}


Pour boucler sur les commentaires, il suffit de faire appel à la fonction for de twig et boucler sur desk.comments. Ce qui nous donne quelque chose de ce style:

Liste des commentaires du bureau via notre relation


Facile à manipuler non ?



Les relations OneToOne et ManyToMany dans Symfony2


Les deux autres relations OneToOne et ManyToMany peuvent aussi être utilisées via des annotations.

Relation OneToOne



Imaginons que notre entité Desk dispose d'une relation OneToOne avec une entité Case (Boitier).
Un bureau ne peux avoir qu'un seul boitier et ce boitier est unique, ne peux appartenir à plusieurs bureaux. Nous avons donc une relation en OneToOne.

Il est possible de créer une relation unidirectionnelle en OneToOne comme ceci:

Desk.php
Code php:
    /**
     * @ORM\OneToOne(targetEntity="Case")
     * @ORM\JoinColumn(name="case_id", referencedColumnName="id")
     */
    private $case;


Pas besoin de lier l'entité Case si vous souhaitez de l'unidirectionnel. Mais si vous voulez la version bidirectionnelle, il faut ajouter la relation dans Case:

Code php:
    /**
     * @ORM\OneToOne(targetEntity="Desk", mappedBy="case")
     */
     private $desk;



Et il existe encore une variante, si vous voulez faire une relation OneToOne sur la même entité. Par exemple pour une entité Category qui peut avoir une catégorie parente:

Category.php
Code php:
    /**
     * @ORM\OneToOne(targetEntity="Category")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
     */
    private $category;




Relation ManyToMany



Prenons pour exemple l'entité Desk et une entité Tag. Un bureau peut être caractérisé par plusieurs tags et un tag peut être associé à plusieurs bureaux. On a donc à faire à une relation ManyToMany.
Voici comment la mettre en place avec les annotations:

Desk.php
Code php:
    /**
     * @ORM\ManyToMany(targetEntity="Tag", inversedBy="desks")
     * @ORM\JoinTable(name="desk_tags")
     */
    private $tags;

    public function __construct() {
        $this->tags = new \Doctrine\Common\Collections\ArrayCollection();
    }


Tag.php
Code php:
    /**
     * @ORM\ManyToMany(targetEntity="Desk", mappedBy="tags")
     */
    private $desks;

    public function __construct() {
        $this->desks = new \Doctrine\Common\Collections\ArrayCollection();
    }


Vous avez là une relation ManyToMany bidirectionnelle. Si vous générez le schema, vous obtiendrez une table desk_tags avec les deux clés primaires desk_id et tag_id.

Si vous voulez encore plus d'infos sur les relations Doctrine 2, je vous invite à consulter la documentation officielle doctrine sur le sujet.

Voilà pour notre chapitre sur les entités ! Nous allons maintenant commencer à développer réellement notre projet Watch My Desk v2.





Rechercher sur la Ferme du web