How to override bundled Doctrine repository in Symfony

Viewed 4026

I have an independent Symfony bundle (installed with Composer) with entities and repositories to share between my applications that connect same database.

Entities are attached to every applications using configuration (yml shown):

doctrine:
    orm:
        mappings:
            acme:
                type: annotation
                dir: %kernel.root_dir%/../vendor/acme/entities/src/Entities
                prefix: Acme\Entities
                alias: Acme

Well, it was the easiest way to include external entities in application, but looks a bit ugly.

Whenever I get repository from entity manager:

$entityManager->getRepository('Acme:User');

I get either preconfigured repository (in entity configuration) or default Doctrine\ORM\EntityRepository.

Now I want to override bundled (or default) repository class for a single entity. Is there any chance to do it with some configuration/extension/etc?

I think, the best looking way is something like:

doctrine:
    orm:
         ....:
             Acme\Entities\User:
                 repositoryClass: My\Super\Repository

Or with tags:

my.super.repository:
    class: My\Super\Repository
    tags:
        - { name: doctrine.custom.repository, entity: Acme\Entities\User }
3 Answers

With current symfony 5.3 and doctrine 2.9.5

In your configuration define the service and doctrine.orm.repository_factory:

doctrine:
    orm:
        #Replace repository factory
        repository_factory: 'MyBundle\Factory\RepositoryFactory'

services:
    MyBundle\Factory\RepositoryFactory:
        arguments: [ '@router', '@translator', '%kernel.secret%' ]

Add you MyBundle/Factory/RepositoryFactory.php file:

<?php declare(strict_types=1);

namespace MyBundle\Factory;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Repository\RepositoryFactory as RepositoryFactoryInterface;
use Doctrine\Persistence\ObjectRepository;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * This factory is used to create default repository objects for entities at runtime.
 */
final class RepositoryFactory implements RepositoryFactoryInterface {
        /**
         * The list of EntityRepository instances
         *
         * @var ObjectRepository[]
         */
        private $repositoryList = [];

        /**
         * The kernel secret
         *
         * @var string
         */
        private $secret;

        /**
         * The RouterInterface instance
         *
         * @var RouterInterface
         */
        private $router;

        /**
         * The TranslatorInterface instance
         *
         * @var TranslatorInterface
         */
        private $translator;

        /**
         * Initializes a new RepositoryFactory instance
         *
         * @param RouterInterface $router The router instance
         * @param TranslatorInterface $translator The TranslatorInterface instance
         * @param string $secret The kernel secret
         */
        public function __construct(RouterInterface $router, TranslatorInterface $translator, string $secret) {
                //Set router
                $this->router = $router;

                //Set secret
                $this->secret = $secret;

                //Set translator
                $this->translator = $translator;
        }

        /**
         * {@inheritdoc}
         */
        public function getRepository(EntityManagerInterface $entityManager, $entityName): ObjectRepository {
                //Set repository hash
                $repositoryHash = $entityManager->getClassMetadata($entityName)->getName() . spl_object_hash($entityManager);

                //With entity repository instance
                if (isset($this->repositoryList[$repositoryHash])) {
                        //Return existing entity repository instance
                        return $this->repositoryList[$repositoryHash];
                }

                //Store and return created entity repository instance
                return $this->repositoryList[$repositoryHash] = $this->createRepository($entityManager, $entityName);
        }

        /**
         * Create a new repository instance for an entity class
         *
         * @param EntityManagerInterface $entityManager The EntityManager instance.
         * @param string $entityName The name of the entity.
         */
        private function createRepository(EntityManagerInterface $entityManager, string $entityName): ObjectRepository {
                //Get class metadata
                $metadata = $entityManager->getClassMetadata($entityName);

                //Get repository class
                $repositoryClass = $metadata->customRepositoryClassName ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName();

                //Return repository class instance
                //XXX: router, translator and secret arguments will be ignored by default
                return new $repositoryClass($entityManager, $metadata, $this->router, $this->translator, $this->secret);
        }
}

Then define your MyBundle/Repository/EntityRepository.php:

<?php declare(strict_types=1);

namespace MyBundle\Repository;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository as BaseEntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * EntityRepository
 *
 * {@inheritdoc}
 */
class EntityRepository extends BaseEntityRepository {
        /**
         * The RouterInterface instance
         *
         * @var RouterInterface
         */
        protected RouterInterface $router;

        /**
         * The table keys array
         *
         * @var array
         */
        protected array $tableKeys;

        /**
         * The table values array
         *
         * @var array
         */
        protected array $tableValues;

        /**
         * The TranslatorInterface instance
         *
         * @var TranslatorInterface
         */
        protected TranslatorInterface $translator;

        /**
         * The kernel secret
         *
         * @var string
         */
        protected string $secret;

        /**
         * Initializes a new LocationRepository instance
         *
         * @param EntityManagerInterface $manager The EntityManagerInterface instance
         * @param ClassMetadata $class The ClassMetadata instance
         * @param RouterInterface $router The router instance
         * @param TranslatorInterface $translator The TranslatorInterface instance
         * @param string $secret The kernel secret
         */
        public function __construct(EntityManagerInterface $manager, ClassMetadata $class, RouterInterface $router, TranslatorInterface $translator, string $secret) {
                //Call parent constructor
                parent::__construct($manager, $class);

                //Set secret
                $this->secret = $secret;

                //Set router
                $this->router = $router;

                //Set slugger
                $this->slugger = $slugger;

                //Set translator
                $this->translator = $translator;

                //Get quote strategy
                $qs = $manager->getConfiguration()->getQuoteStrategy();
                $dp = $manager->getConnection()->getDatabasePlatform();

                //Set quoted table names
                //XXX: remember to place longer prefix before shorter to avoid strange replacings
                $tables = [
                        'MyBundle:UserGroup' => $qs->getJoinTableName($manager->getClassMetadata('MyBundle:User')->getAssociationMapping('groups'), $manager->getClassMetadata('MyBundle:User'), $dp),
                        'MyBundle:Group' => $qs->getTableName($manager->getClassMetadata('MyBundle:Group'), $dp),
                        'MyBundle:User' => $qs->getTableName($manager->getClassMetadata('MyBundle:User'), $dp),
                        //XXX: Set limit used to workaround mariadb subselect optimization
                        ':limit' => PHP_INT_MAX,
                        "\t" => '',
                        "\n" => ' '
                ];

                //Set quoted table name keys
                $this->tableKeys = array_keys($tables);

                //Set quoted table name values
                $this->tableValues = array_values($tables);
        }
}

Then simply extend it in MyBundle/Repository/UserRepository.php:

<?php declare(strict_types=1);

namespace MyBundle\Repository;

/**
 * UserRepository
 */
class UserRepository extends EntityRepository {
}
Related