Auto-configuration des dépôts Doctrine en tant que services

Publié le 19/02/2020 • Mis à jour le 14/03/2020

Dans cet article, nous allons voir comment faire en sorte pour que dans un projet Symfony, les dépôts Doctrine soient automatiquement configurés comme services sans avoir à ajouter la moindre configuration. C'est parti ! 😎


English language detected! 🇬🇧

  We noticed that your browser is using English. Do you want to read this post in this language?

Read the english version 🇬🇧

» Publié dans "Une semaine Symfonique 686" (du 17 au 23 février 2020) et Le PHP annoté Jetbrains de mars 2020.

Prérequis

J'assumerai que vous savez ce qu'est un dépôt Doctrine et comment l'utiliser dans une application Symfony pour récupérer des informations d'une base de données.

Configuration

  • PHP 7.4
  • Symfony 5.0.7 avec l'autowiring activé.

Introduction

Parfois, quand on développe et qu'on sait faire quelque chose, il est tentant de reproduire ce qu'on a déjà fait par le passé sans se remettre en question et vérifier s'il n'y pas une autre manière de procéder (c'est maaaaal !). J'ai récemment migré ce blog sous Symfony 5 (non sans peine). Quand j'ai vérifié le fichier de configuration doctrine.yaml il contenait ces quelques lignes (Je les ai commentées. Je les ai laissées uniquement pour la compréhension de cet article) :

# config/packages/doctrine.yaml
#services:
#    App\Entity\ArticleRepository:
#        factory: ['@doctrine.orm.default_entity_manager', getRepository]
#        arguments:
#            - App\Entity\Article

Je me suis dit : j'ai Symfony 5 avec Flex, l'autowiring... Ai-je besoin de déclarer les dépôts Doctrine manuellement ? La réponse est non, nous n'avons pas à le faire. Ensuite, je me suis souvenu que j'ai lu il y un moment un article de Tomas Vortuba à ce sujet. Cet article a deux ans, mais ça vaut le coup de le lire (avec les commentaires). Il explique les différentes stratégies possibles avec pour chacune, les avantages et inconvénients. Dans les commentaires, un certain Melyou a souligné l'existence de la classe Doctrine ServiceEntityRepository et quelqu'un a ajouté quelques explications à son sujet. Je devais donc creuser dans cette direction et faire un essai.

Mon but était donc clair, supprimer cette configuration inutile. Si vous avez le même type de configuration, voyons comment faire. Si vous créez une nouvelle entité, lancez la commande maker:entity et vous êtes bon (vous devrez donc avoir le MakerBundle installé).

Extraire les dépôts du répertoire entity

Par le passé, nous avions l'habitude (c'était mon cas) de mettre les dépôts Doctrine dans le même répertoire que les entités. Ceux-ci ne sont pas chargés automatiquement à cause de la configuration par défaut de l'autowiring du fichier config/services.yaml où le répertoire Entity est exclu :

            # https://symfony.com/doc/current/profiler.html#enabling-the-profiler-conditionally
            $profiler: '@?profiler'

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name

Donc, la première étape est de les déplacer dans un autre répertoire. Prenons la même convention que celle choisie par le bundle Maker à savoir le répertoire src/Repository :

<?php declare(strict_types=1);

namespace App\Repository;

use App\Entity\Article;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * @method Article|null findOneBySlug(string $slug)
 */
final class ArticleRepository extends ServiceEntityRepository
{
    use BaseRepositoryTrait;

    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Article::class);
    }

L'espace de nom est maintenant App\Repository au lieu de App\Entity. La principale différence avec la version antérieure est que nous étendons désormais la classe ServiceEntityRepository. Nous devons ajouter un constructeur qui va nous permettre de lier le bon type d'entité au dépôt. C'est l'équivalent de l'appel factory que nous faisions dans le fichier de configuration Doctrine. C'est bien plus propre désormais, tout est configuré au même endroit. Le reste du code reste quant à lui identique.

Cliquez ici pour voir la code de la classe Doctrine ServiceEntityRepository.
<?php

namespace Doctrine\Bundle\DoctrineBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use LogicException;

/**
 * Optional EntityRepository base class with a simplified constructor (for autowiring).
 *
 * To use in your class, inject the "registry" service and call
 * the parent constructor. For example:
 *
 * class YourEntityRepository extends ServiceEntityRepository
 * {
 *     public function __construct(ManagerRegistry $registry)
 *     {
 *         parent::__construct($registry, YourEntity::class);
 *     }
 * }
 */
class ServiceEntityRepository extends EntityRepository implements ServiceEntityRepositoryInterface
{
    /**
     * @param string $entityClass The class name of the entity this repository manages
     */
    public function __construct(ManagerRegistry $registry, $entityClass)
    {
        $manager = $registry->getManagerForClass($entityClass);

        if ($manager === null) {
            throw new LogicException(sprintf(
                'Could not find the entity manager for class "%s". Check your Doctrine configuration to make sure it is configured to load this entity’s metadata.',
                $entityClass
            ));
        }

        parent::__construct($manager, $manager->getClassMetadata($entityClass));
    }
}

Ajouter des méthodes génériques aux dépôts

Comme le dépôt étend désormais la classe ServiceEntityRepository, j'utilise un trait BaseRepositoryTrait pour ajouter quelques méthodes génériques, jetons-y un coup d'œil :

<?php declare(strict_types=1);

namespace App\Repository;

use Doctrine\ORM\EntityManagerInterface;

/**
 * @method EntityManagerInterface getEntityManager()
 */
trait BaseRepositoryTrait
{
    /**
     * This is just an example. Use ->count([]) for the same result.
     */
    public function countAll(): int
    {
        return (int) $this->getEntityManager()
            ->createQuery(sprintf('SELECT COUNT(a) FROM %s a', $this->getClassName()))
            ->getSingleScalarResult();
    }

Pour activer l'autocomplétion dans le trait, l'astuce est d'ajouter des annotations @method ou @property pour que votre IDE sache quel type d'objet vous utilisez. Dans ce cas on indique que la fonction getEntityManager() retourne un objet de type EntityManagerInterface. C'est quelque chose qui est aussi utilisé dans le code Symfony, voici un exemple (regardez la dernière ligne du snippet suivant) :

Cliquez ici pour voir le code du trait Symfony CompiledUrlMatcherTrait.php.
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Routing\Matcher\Dumper;

use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
use Symfony\Component\Routing\RequestContext;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 *
 * @property RequestContext $context
 */
trait CompiledUrlMatcherTrait
{
    private $matchHost = false;
    private $staticRoutes = [];
    private $regexpList = [];
    private $dynamicRoutes = [];
    private $checkCondition;

    public function match(string $pathinfo): array
    {
        $allow = $allowSchemes = [];
        if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
            return $ret;
        }
        if ($allow) {
            throw new MethodNotAllowedException(array_keys($allow));
        }
        if (!$this instanceof RedirectableUrlMatcherInterface) {
            throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
        }
        if (!\in_array($this->context->getMethod(), ['HEAD', 'GET'], true)) {

Utiliser le dépôt comme un service

Rien de particulier, le dépôt est désormais un service comme les autres. Par exemple, regardons le contrôleur responsable de l'affichage de la page d'accueil de ce site :

    /**
     * @Route({"en": "/en", "fr": "/fr"}, name="homepage")
     */
    public function homepage(string $_locale, ArticleRepository $articleRepository, array $goals): Response
    {
        $data = [];
        $date = new \DateTime();
        $data['goals'] = $goals;
        $data['article'] = $articleRepository->findLastArticleForLang($_locale);
        $data['snippet'] = $articleRepository->findLastSnippetForLang($_locale);
        $data['done'] = $articleRepository->getDoneGoals();
        $data['year_day'] = (int) $date->format('z')+1;
        $data['year_percent'] = $data['year_day']/365*100;
        $data['week_number'] = (int) $date->format('W');

On injecte le service dans les paramètres de la méthode du contrôleur avec le typehint adéquat. Et voilà ! Maintenant, à chaque fois que nous déclarerons un nouveau dépôt comme cela, il sera automatiquement disponible sans avoir à modifier la configuration.

Conclusion

Je viens juste de faire cette modification dans ce projet. Voyons comment ça se passe à l'usage. Mais pour moi, il semble clair que c'est plus propre que ce que j'avais l'habitude de faire. Ajouter une entrée dans le fichier des services était laborieux et n'apportait aucun avantage. Le fait que le bundle Maker utilise cette approche montre que c'est considéré comme une meilleure pratique.

Et voilà ! J'espère que vous avez aimé. Découvrez d'autres informations en rapport à cet article avec les liens ci-dessous. Comme toujours, retours, likes et retweets sont les bienvenus. (voir la boîte ci-dessous) À la revoyure ! COil. 😊

  Lire la doc  Plus sur le web  Plus sur Stackoverflow

Ils m'ont donné leurs retours et m'ont aidé à corriger des erreurs et typos dans cet article, un grand merci à : jmsche, TimoBakx, gubler. 👍


A vous de jouer !

Ces articles vous ont été utiles ? Vous pouvez m'aider à votre tour de plusieurs manières : (cf le tweet à droite pour me contacter )

  • Me remonter des erreurs ou typos.
  • Me remonter des choses qui pourraient être améliorées.
  • Aimez et retweetez !
  • Suivez moi sur Twitter
  • Inscrivez-vous au flux RSS.

Merci d'avoir tenu jusque ici et à très bientôt sur Strangebuzz ! 😉

COil