Les repository et les managers d'entité dans Symfony2

Nous avons déjà évoqué l'utilisation des repository mais n'avons pas encore vu comment s'en servir concrètement. C'est donc l'objet de ce chapitre, dans lequel nous parlerons aussi des Managers d'entité.




Les repository d'entités dans Symfony2


Commençons par voir comment utiliser correctement les repository d'entités avec Symfony2.
Lorsque nous avons généré nos deux entités, nous avons en même temps créé leur repository respectif dans le répertoire Entity.

Pour plus de clarté, nous allons créer un répertoire src/Wmd/WatchMyDeskBundle/Repository/.
Ensuite déplacez les deux repository dans le dossier puis modifier le namespace des deux fichiers:

Au lieu de:
Code php:
namespace Wmd\WatchMyDeskBundle\Entity;

Mettez:
Code php:
namespace Wmd\WatchMyDeskBundle\Repository;


Et changeons à présent la référence vers les repository dans les deux entités.
Pour Desk.php:
Code php:
@ORM\Entity(repositoryClass="Wmd\WatchMyDeskBundle\Repository\DeskRepository")


Pour DeskComment.php
Code php:
@ORM\Entity(repositoryClass="Wmd\WatchMyDeskBundle\Repository\DeskCommentRepository")


Vous pouvez créer autant de répertoires que vous le souhaitez dans le bundle donc n'hésitez pas à ordonner vos fichiers.


Que mettre dans les repository ?



On parle des repository, mais on a pas encore vu leur réelle fonction.
Pour faire simple, on va dire que tout ce qui touche les requêtes en base de données devra se situer dans les repository.
C'est à cet endroit que l'on va construire nos requêtes doctrine grâce au queryBuilder.

Pour illustrer, nous allons créer une méthode dans le DeskRepository permettant de récupérer les XX derniers bureaux publiés et validés.
Pour créer une requête doctrine, nous avons deux possibilités:
  • Créer nous même la requête grâce à la méthode createQuery de l'entity manager
  • Créer la requête grâce au QueryBuilder


La première méthode, en récupérant l'entity manager stockée dans la propriété _em de la classe:
Code php:
    public function getLastPosted($limit = 5)
    {
        return $this->_em->createQuery('
            SELECT
                d
            FROM
                WmdWatchMyDeskBundle:Desk d
            WHERE
                d.isEnabled = :enabled
            ORDER BY
                d.updatedAt DESC,
                d.id DESC
        ')
        ->setMaxResults($limit)
        ->setParameter('enabled', true);
    }


Cette méthode peut être pratique lorsque l'on souhaite utiliser directement des fonctions MySQL qui ne sont pas supportés par le queryBuilder.

Attention toutefois, cette façon de coder les requêtes peut entrainer des incompatibilités si vous souhaitez changer de base de données.



La deuxième façon de faire avec le queryBuilder doctrine:
Code php:
    public function getLastPosted($limit = 5)
    {
        return $this->createQueryBuilder('d')
        ->where('d.isEnabled = :enabled')
        ->setParameter('enabled', true)
        ->addOrderBy('d.updatedAt', "DESC")
        ->addOrderBy('d.id', "DESC")
        ->setMaxResults($limit);
    }


Vous remarquerez qu'on ne retourne pas la requête avec le getQuery(). On ne retourne que le queryBuilder, ce qui nous permettra de pouvoir facilement ajouter des paramètres à notre requête dans les managers (Il est tout à fait possible de renvoyer la query, mais moins pratique si vous designez votre projet comme nous allons le faire).

Pour en savoir plus sur le queryBuilder, rendez-vous sur la doc officielle doctrine.

Les managers d'entités dans Symfony2


Pour faciliter la gestion de nos entités, nous allons centraliser les actions dans des Managers.

Il s'agit d'une bonne pratique qui permettra d'alléger nos contrôleurs qui doivent en général être les plus concis possible.
Comme leur nom l'indique ils permettront aussi de manipuler plus facilement nos entités en faisant le lien entre les repository et l'entité, via des méthodes helpers pratiques.

Managers dans Symfony


Avantages des Managers d'entités

  • Allègent les contrôleurs
  • Formatent les données pour les passer au contrôleur
  • Appellent les méthodes des Repository
  • Facilite les tests unitaires et le coverage
  • Agissent comme proxy et proposent des méthodes helpers


Tous nos managers vont implémenter la classe abstraite BaseManager.php que l'on va développer nous même et placer dans un répertoire Wmd/WatchMyDeskBundle/Manager/:

Code php:
<?php

namespace Wmd\WatchMyDeskBundle\Manager;

abstract class BaseManager
{
    protected function persistAndFlush($entity)
    {
        $this->em->persist($entity);
        $this->em->flush();
    }
}


Pour le moment, seule une méthode persistAndFlush sera disponible dans la classe, elle permettra de sauvegarder notre objet en BDD.

Passons à la création du DeskManager à présent.



Création du DeskManager.php


Créez un nouveau fichier Manager/DeskManager.php.

Code php:
<?php

namespace Wmd\WatchMyDeskBundle\Manager;

use Doctrine\ORM\EntityManager;
use Wmd\WatchMyDeskBundle\Manager\BaseManager;
use Wmd\WatchMyDeskBundle\Entity\Desk;

class DeskManager extends BaseManager
{
    protected $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function loadDesk($deskId) {
        return $this->getRepository()
                ->findOneBy(array('id' => $deskId));
    }

    /**
    * Save Desk entity
    *
    * @param Desk $desk 
    */
    public function saveDesk(Desk $desk)
    {
        $this->persistAndFlush($desk);
    }

    public function getPreviousDesk($deskId) {
        return $this->getRepository()
                ->getAdjacentDesk($deskId, false)
                ->getQuery()
                ->setMaxResults(1)
                ->getOneOrNullResult();
    }

    public function getNextDesk($deskId) {
        return $this->getRepository()
                ->getAdjacentDesk($deskId, true)
                ->getQuery()
                ->setMaxResults(1)
                ->getOneOrNullResult();
    }

    public function isAuthorized(Desk $desk, $memberId)
    {
        return ($desk->getMember()->getId() == $memberId) ?
                true:
                false;
    }

    public function getPreviousAndNextDesk($desk)
    {
        return array(
            'prev' => $this->getPreviousDesk($desk->getId()),
            'desk' => $desk,
            'next' => $this->getNextDesk($desk->getId()),
            'voted' => false
        );
    }

    public function getRepository()
    {
        return $this->em->getRepository('WmdWatchMyDeskBundle:Desk');
    }

}


Nous avons créé plusieurs méthodes de base qui vont nous être utiles pour gérer notre entité Desk:
  • getRepository: Permet uniquement de récupérer le repository de l'entité
  • loadDesk: Permet de récupérer un bureau à partir d'un ID
  • getPreviousDesk: Nous permettra de récupérer le bureau précédent (Pour la navigation dans le détail d'un bureau)
  • getNextDesk: Nous permettra de récupérer le bureau suivant (Pour la navigation dans le détail d'un bureau)
  • getPreviousAndNextDesk: Permet de renvoyer un tableau avec le bureau suivant et précédent pour gérer la navigation.
  • isAuthorized: Permet de tester si un membre à les droits de modification sur un bureau


Les plus curieux d'entre vous se sont sans doute posé la question: Comment allons nous passer dans le constructeur du Manager l'EntityManager ?

La réponse: En transformant notre Manager en service !


Les services dans Symfony2


Symfony2 introduit un nouveau concept par rapport aux version 1.x: Le service container.

Qu'est-ce qu'un service ?



Les services dans Symfony2


Un service est une tâche / fonction disponible de manière globale dans le framework.
Tous les services sont disponibles via le service container, un objet utilisé par le core de Symfony2, principal responsable des performances en rapidité et extensibilité du framework (Nous verrons plus tard la notion d'injection de dépendance).

Les services vont par exemple nous permettre de récupérer l'objet responsable de l'envoi des emails, celui qui va gérer les templates etc.

Passons à la pratique pour mieux comprendre l'utilité d'un service en créant le service de notre DeskManager.

Les services sont contenus dans un fichier de configuration XML situé à cet endroit: Wmd/WatchMyDeskBundle/Resources/config/services.xml. En théorie, ce fichier est généré par défaut à la création du bundle, créez le si ce n'est pas le cas.

Lorsque vous ouvrez le fichier, vous devriez obtenir quelque chose comme ceci:
Code xml:
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <!--
    <parameters>
        <parameter key="wmd_watch_my_desk.example.class">Wmd\WatchMyDeskBundle\Example</parameter>
    </parameters>

    <services>
        <service id="wmd_watch_my_desk.example" class="%wmd_watch_my_desk.example.class%">
            <argument type="service" id="service_id" />
            <argument>plain_value</argument>
            <argument>%parameter_name%</argument>
        </service>
    </services>
    -->
</container>


Le fichier est composé de deux grandes parties:
  • Les services déclarés de notre bundle dans <services> dans lesquels nous passons les différents arguments qu'ils peuvent recevoir (paramètre du __construct).
  • Les paramètres dans <parameters> qui vont permettre de dire quels sont les objets à passer pour les arguments déclaré dans les services.


Comment ça, ce n'est pas très clair ?
Bon ok, passons à la pratique, modifions le fichier pour déclarer notre service DeskManager.

Si vous vous souvenez bien, le constructeur du manager prend en argument EntityManager $em. Donc le service va prendre un argument:

Code xml:
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="wmd.desk_manager.class">Wmd\WatchMyDeskBundle\Manager\DeskManager</parameter>
    </parameters>

    <services>
        <service id="wmd.desk_manager" class="%wmd.desk_manager.class%">
            <argument type="service" id="doctrine.orm.entity_manager" />
        </service>
    </services>

</container>


C'est aussi simple que ça !

A présent dans nos contrôleurs, nous allons pouvoir faire appel à notre service dont l'id est wmd.desk_manager.

Ouvrez le fichier Controller/DeskController.php et modifiez l'action show ainsi:

Code php:
    /**
     * @Route("/show/{deskId}", name="desk_show")
     * @Template()
     */
    public function showAction($deskId)
    {
        if (!$desk = $this->get('wmd.desk_manager')->loadDesk($deskId)) {
            throw new NotFoundHttpException($this->get('translator')->trans('This desk does not exist.'));
        }
        
        return array('desk' => $desk);
    }


Comme vous le voyez, nous utilisons la méthode get du contrôleur, qui est un alias de la propriété $this->container, pour accéder aux services et récupérer ainsi notre wmd.desk_manager:
Code php:
$this->get('wmd.desk_manager')


Alors, nous pouvons appeler une de ses méthodes, comme loadDesk qui va récupérer le bureau à partir d'un ID donné.
Si aucun bureau n'est associé à cet ID, alors on renvoi une erreur 404 en déclenchant une exception NotFoundHttpException.
Pour que l'exception fonctionne pensez à ajouter le namespace suivant en haut du contrôleur:
Code php:
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;


Modifions maintenant le template Twig qui affiche le détail du bureau (Resources/views/Desk/show.html.twig):

Code html:
{% extends '::base.html.twig' %}

{% block title %}
    {{ parent() }}
    - {{ desk.title }}
{% endblock %}

{% block body %}
    <h1>{{ desk.title }}</h1>
    <p>
        {{ desk.description }}
    </p>
    <p>
        Bureau proposé le {{ desk.createdAt|date('d/m/Y') }}
    </p>
    <a href="{{ path("homepage") }}">Retour à l'accueil</a>
{% endblock %}


C'est l'heure de tester maintenant !
Essayons déjà avec un bureau qui n'existerait pas, dans mon cas, le Desk d'id 5 n'existe pas: http://dev.watchmydesk.com/app_dev.php/show/5
Erreur 404 sous Symfony2


Voici l'erreur que j'ai obtenu (en mode app_dev.php).

Vous constaterez que le nom de l'erreur s'affiche.
C'est très pratique pour debug, je vous invite à être le plus verbeux possible dans le nom de vos erreurs pour comprendre tout de suite le problème.



Affichage du détail du bureau avec le Manager Symfony


Notre bureau est correctement affiché avec un ID existant.

Vous voyez, ce n'était pas sorcier!
Nous avons codé proprement, sans alourdir notre contrôleur.
Le plus gros avantage reste la flexibilité: Si pour une raison X ou Y nous souhaitons changer de classe Manager pour gérer les bureau, il nous suffirait d'aller dans le fichier config/services.xml et changer une seule ligne.
Exemple, si on souhaite utiliser DeskRevolutionManager au lieu de DeskManager:
Code xml:
<parameter key="wmd.desk_manager.class">Wmd\WatchMyDeskBundle\Manager\DeskRevolutionManager</parameter>


C'est tout de suite opérationnel, et vous n'aurez rien à changer dans vos contrôleurs ou commandes etc. Pratique non ? Et bien ça s'appelle l'injection de dépendance :)






Rechercher sur la Ferme du web