Techos Blog technique de Xotelia

Mapping driver personnalisé pour Doctrine ORM

Récemment j’ai mis en place Doctrine comme ORM à la place du moteur maison. Ce n’a pas été de tout repos. Il faut dire que si on veut utiliser un driver personnalisé pour Doctrine ORM, il faut mettres les mains dans le camboui.

try/catch

Pour commencer, la documentation officielle n’est pas à jour. Au lieu de devoir implémenter l’interface Doctrine\ORM\Mapping\Driver\Driver il faut implémenter celle-ci Doctrine\Common\Persistence\Mapping\Driver\MappingDriver. Aussi, ce n’est plus la classe Doctrine\ORM\Mapping\ClassMetadataInfo mais l’interface Doctrine\Common\Persistence\Mapping\ClassMetadata qu’il faut utiliser. Dont l’implémentation est Doctrine\ORM\Mapping\ClassMetadata.

Deuxièmement, il existe un Doctrine\Common\Persistence\Mapping\Driver\FileDriver pour faciliter la gestion des mappings par fichier. Dans le cas de Xotelia ça n’a pas aidé car le mapping pour un modèle est fait avec deux fichiers. Le premier pour les champs et les relations, le deuxième pour la correspondance entre le nom des champs et des tables.

Alors j’implémente le nouveau driver, en étudiant l’ORM actuel pour être “ISO”.

Mise en pratique

Une fois l’implémentation terminée, il est temps de connecter le nouveau driver pour vérifier que tout fonctionne. On utilise Symfony donc il est temps de regarder la documentation. La méthode indiquée sur la documentation de Doctrine ne peut pas servir dans ce cas. Premièrement, nous avons plusieurs bundles qui utilisent déjà doctrine. Donc on ne peut pas purement et simplement remplacer le driver actuel qui est Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain par notre driver personnalisé.

Ne trouvant aucune documentation sur la marche à suivre, j’essaye la méthode de mapping manuelle de symfony ici. En essayant ensuite la commande doctrine:mapping:info on obtient l’erreur suivante :

Fatal error: Uncaught exception 'InvalidArgumentException' with message 'Can only configure "xml", "yml", "annotation", "php" or "staticphp" through the DoctrineBundle. Use your own bundle to configure other metadata drivers. You can register them by adding a new driver to the "doctrine.orm.default.metadata_driver" service definition.' in /vagrant/vendor/symfony/symfony/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php on line 252

InvalidArgumentException: Can only configure "xml", "yml", "annotation", "php" or "staticphp" through the DoctrineBundle. Use your own bundle to configure other metadata drivers. You can register them by adding a new driver to the "doctrine.orm.default.metadata_driver" service definition. in /vagrant/vendor/symfony/symfony/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php on line 252

Nous apprenons donc que l’on ne peut pas utiliser la configuration du bundle doctrine pour rajouter notre propre driver. Mais qu’il faut ajouter notre driver au service doctrine.orm.default.metadata_driver. En lançant la commande debug:container, impossible de trouver le service en question. Les choses se corsent. En fouillant un peu dans le code de symfony, on trouve dans Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension le moment ou les drivers sont instanciés. Et on voit que si le service en question existe, on connecte les driver à celui-ci, sinon on crée un service privé.

Seulement, quand on regarde dans l’erreur on indique le service doctrine.orm.default.metadata_driver alors que lors de la déclaration dans le code, c’est le service doctrine.orm.default_metadata_driver qui est recherché. Avec un _ après default au lieu d’un ..

La solution est donc de créer le-dit service pour ensuite pouvoir y connecter notre driver. Rien de plus simple :

services:
    doctrine.orm.default_metadata_driver:
        class: "%doctrine.orm.metadata.driver_chain.class%"

Et pour finir, il faut utiliser la méthode suivante addDriver(MappingDriver $nestedDriver, $namespace) pour connecter notre driver. On va utiliser les CompilerPass pour ça :

<?php

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Definition;

class DriverCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $definition = $container->findDefinition('doctrine.orm.default_metadata_driver');
        $definition->addMethodCall('addDriver', [
            new Definition('My\\Custom\\Driver', []),
            'My\\Namespace',
        ]);
    }
}

Ne pas oublier d’importer ce CompilerPass dans notre bundle :

<?php

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use AppBundle\DependencyInjection\Compiler\DriverCompilerPass;

class AppBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new DriverCompilerPass());
    }
}

Et là, le tour est joué, notre driver est enfin fonctionnel. On aurrait très bien pu aussi rajouter le call directement dans la définition du service en yaml.