Hubert Perron
Hubert Perron

Reputation: 3022

Injecting dependency into entity repository

Is there a simple way to inject a dependency into every repository instance in Doctrine2 ?

I have tried listening to the loadClassMetadata event and using setter injection on the repository but this naturally resulted in a infinite loop as calling getRepository in the event triggered the same event.

After taking a look at the Doctrine\ORM\EntityManager::getRepository method it seems like repositories are not using dependency injection at all, instead they are instantiated at the function level:

public function getRepository($entityName)
{
    $entityName = ltrim($entityName, '\\');
    if (isset($this->repositories[$entityName])) {
        return $this->repositories[$entityName];
    }

    $metadata = $this->getClassMetadata($entityName);
    $customRepositoryClassName = $metadata->customRepositoryClassName;

    if ($customRepositoryClassName !== null) {
        $repository = new $customRepositoryClassName($this, $metadata);
    } else {
        $repository = new EntityRepository($this, $metadata);
    }

    $this->repositories[$entityName] = $repository;

    return $repository;
}

Any ideas ?

Upvotes: 28

Views: 24432

Answers (6)

Tomas Votruba
Tomas Votruba

Reputation: 24280

Since Symfony 3.3+ and 2017 you can make use of services.


Instead of other proposed solutions here that lead to:

  • hacking the repository factory
  • making service configuration in YAML
  • and creating a lot of boilerplate code that will hunt you down later

You can do it...


Clean Way - Dependency via Constructor Injection like in any other Service

<?php declare(strict_types=1);

namespace App\Repository;

use App\Entity\Post;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;

final class PostRepository
{
    /**
     * @var EntityRepository
     */
    private $repository;

    /**
     * @var YourOwnDependency
     */
    private $yourOwnDependency;

    public function __construct(YourOwnDependency $YourOwnDependency, EntityManager $entityManager)
    {
        $this->repository = $entityManager->getRepository(Post::class);

        $this->yourOwnDependency = $yourOwnDependency
    }
}


Read More in the Post

You can read more detailed tutorial with clear code examples in How to use Repository with Doctrine as Service in Symfony post.

Upvotes: 2

cadavre
cadavre

Reputation: 1394

You can actually create your own DefaultRepository extends EntityRepository, construct it with all dependencies you need and then set it as a default Repository with:

doctrine:
    orm:
        entity_managers:
            default:
                default_repository_class: AppBundle\ORM\DefaultRepository

Upvotes: -2

samanime
samanime

Reputation: 26527

This is a YAML version of Aldo's answer, just in case you are using YAML configurations instead of XML

your_namespace.repository.repos_name:
    class: %your_namespace.repository.repos_name%
    factory: ["@doctrine", getRepository]
    arguments:
        - entity_name
        - entity_manager_name
    calls:
        - [setContainer, ["@service_container"]]

And prior to version 2.8:

your_namespace.repository.repos_name:
    class: %your_namespace.repository.repos_name%
    factory_service: doctrine
    factory_method: getRepository
    arguments:
        - entity_name
        - entity_manager_name
    calls:
        - [setContainer, [@service_container]]

Also, as a note, entity_manager_name is an optional parameter. I want the default for my particular use, so I just left it blank (just in case I ever rename the default manager).

Upvotes: 16

Vladimir Pak
Vladimir Pak

Reputation: 702

I've just define my own RepositoryFactory class

  1. Create RepositoryFactory class and define service, for example my_service.orm_repository.robo_repository_factory, with include @service_container injection
  2. And add check and set container service, for instance:

    private function createRepository(EntityManagerInterface $entityManager, $entityName)
    {
        /* @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */
        $metadata = $entityManager->getClassMetadata($entityName);
        $repositoryClassName = $metadata->customRepositoryClassName
            ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName();
    
        $result = new $repositoryClassName($entityManager, $metadata);
        if ($result instanceof ContainerAwareInterface) {
            $result->setContainer($this->container);
        }
        return $result;
    }
    
  3. Create compiler class

    public function process(ContainerBuilder $container)
    {
        $def = $container->getDefinition('doctrine.orm.configuration');
        $def->addMethodCall(
            'setRepositoryFactory', [new Reference('robo_doctrine.orm_repository.robo_repository_factory')]
        );
    }
    
  4. After that any EntityRepository with ContainerAwareInterface has @service_container

Upvotes: 1

Steven Mercatante
Steven Mercatante

Reputation: 25295

If you use a custom EntityManager you could override the getRepository method. Since this doesn't involve the loadClassMetadata event, you won't run into an infinite loop.

You would first have to pass the dependency to your custom EntityManager, and then you'd pass it to the repository object using setter injection.

I answered how to use a custom EntityManager here, but I'll replicate the answer below:

1 - Override the doctrine.orm.entity_manager.class parameter to point to your custom entity manager (which should extend Doctrine\ORM\EntityManager.)

2 - Your custom entity manager must override the create method so that it returns an instance of your class. See my example below, and note the last line regarding MyEntityManager:

public static function create($conn, Configuration $config, EventManager $eventManager = null) {
        if (!$config->getMetadataDriverImpl()) {
            throw ORMException::missingMappingDriverImpl();
        }

        if (is_array($conn)) {
            $conn = \Doctrine\DBAL\DriverManager::getConnection($conn, $config, ($eventManager ? : new EventManager()));
        } else if ($conn instanceof Connection) {
            if ($eventManager !== null && $conn->getEventManager() !== $eventManager) {
                throw ORMException::mismatchedEventManager();
            }
        } else {
            throw new \InvalidArgumentException("Invalid argument: " . $conn);
        }

        // This is where you return an instance of your custom class!
        return new MyEntityManager($conn, $config, $conn->getEventManager());
    }

You'll also need to use the following in your class:

use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\ORMException;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;

Edit

Since the default entity manager is created from the create method, you can't simply inject a service into it. But since you're making a custom entity manager, you can wire it up to the service container and inject whatever dependencies you need.

Then from within the overridden getRepository method you could do something like
$repository->setFoo($this->foo). That's a very simple example - you may want to first check if $repository has a setFoo method before calling it. The implementation is up to you, but this shows how to use setter injection for a repository.

Upvotes: 10

Aldo Stracquadanio
Aldo Stracquadanio

Reputation: 6237

Problem is that repository classes are not part of the Symfony2 codebase as they are part of Doctrine2, so they do not take advantage of the DIC; this is why you can't go for injection in one place for all repositories.

I would advice you to use a different approach. For example you can create a service layer on top of the repositories and actually inject the class you want through a factory in that layer.

Otherwise you could also define repositories as services this way:

<service id="your_namespace.repository.repos_name"
          class="%your_namespace.repository.repos_name%"
          factory-service="doctrine" factory-method="getRepository">
  <argument>entity_name</argument>
  <argument>entity_manager_name</argument>
  <call method="yourSetter">
      <argument>your_argument</argument>
  </call>
</service>

A solution that could centralize the set method call is to write a DIC tag and a compiler pass to handle it and tag all repository services.

Upvotes: 35

Related Questions