Symfony - inject doctrine repository in service

Check the arguments is a valid class (with FQCN or with a bundle simplification) as example:

acme.custom_repository:
    class: Doctrine\ORM\EntityRepository
    factory: 
        - '@doctrine.orm.entity_manager'
        - getRepository
    arguments:
        - Acme\MainBundle\Entity\MyEntity

or

acme.custom_repository:
    class: Doctrine\ORM\EntityRepository
    factory: 
        - '@doctrine.orm.entity_manager'
        - getRepository
    arguments:
        - AcmeMainBundle:MyEntity

Hope this help


As you are using Symfony 3.4, you can use a much simpler approach, using ServiceEntityRepository. Simply implement your repository, let it extend class ServiceEntityRepository and you can simply inject it. (At least when using autowiring – I haven’t used this with classic DI configuration, but would assume it should also work.)

In other words:

namespace App\Repository;

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class ExampleRepository extends ServiceEntityRepository
{
    /**
     * @param ManagerRegistry $managerRegistry
     */
    public function __construct(ManagerRegistry $managerRegistry)
    {
        parent::__construct($managerRegistry, YourEntity::class);
    }
}

Now, without any DI configuration, you can inject the repository wherever you want, including controller methods.

One caveat (which equally applies to the way you try to inject the repository): if the Doctrine connection is reset, you will have a reference to a stale repository. But IMHO, this is a risk I accept, as otherwise I won’t be able to inject the repository directly..


Solutions I could see here so far are not bad. I looked at it from a different angle. So my solution allows you to keep clean repositories, sorta enforces consistent project structure and you get to keep autowiring!

This is how I would solve it in Symfony 5.

GOAL

We want to have autowired Repositories and we want to keep them as clean as possible. We also want them to be super easy to use.

PROBLEM

We need to figure out a way to tell Repository about the entity it should use.

SOLUTION

The solution is simple and consists of a few things:

  1. We have custom Repository class which extends Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository class.
  2. Our custom class has public string $entity property on it.
  3. When we create our new repository and extend our custom repository class we have two choices: on our new repository we can just point to the class like this

    namespace App\Database\Repository\Post;
    
    use App\Database\Repository\Repository;
    use App\Entity\Blog\Post;
    
    /**
     * Class PostRepository
     * @package App\Database\Repository
     */
    class PostRepository extends Repository
    {
        public string $entity = Post::class;
    
        public function test()
        {
            dd(99999, $this->getEntityName());
        }
    }
    

or we could omit that property and let our new base Repository class find it automatically! (More about that later.)

CODE

So let's start with the code and then I will explain it:

<?php

namespace App\Database\Repository;

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Laminas\Code\Reflection\ClassReflection;
use Symfony\Component\Finder\Finder;

/**
 * Class Repository
 * @package App\Database\Repository
 */
abstract class Repository extends ServiceEntityRepository
{
    /** @var string  */
    private const REPOSITORY_FILE = 'repository';

    /** @var string */
    public string $entity = '';
    /** @var string */
    public string $defaultEntitiesLocation;
    /** @var string */
    public string $defaultEntitiesNamespace;

    /**
     * Repository constructor.
     *
     * @param ManagerRegistry $registry
     * @param $defaultEntitiesLocation
     * @param $defaultEntitiesNamespace
     * @throws \Exception
     */
    public function __construct(
        ManagerRegistry $registry,
        $defaultEntitiesLocation,
        $defaultEntitiesNamespace
    ) {
        $this->defaultEntitiesLocation = $defaultEntitiesLocation;
        $this->defaultEntitiesNamespace = $defaultEntitiesNamespace;
        $this->findEntities();
        parent::__construct($registry, $this->entity);
    }

    /**
     * Find entities.
     *
     * @return bool
     * @throws \ReflectionException
     */
    public function findEntities()
    {
        if (class_exists($this->entity)) {
            return true;
        }
        $repositoryReflection = (new ClassReflection($this));
        $repositoryName = strtolower(preg_replace('/Repository/', '', $repositoryReflection->getShortName()));
        $finder = new Finder();
        if ($finder->files()->in($this->defaultEntitiesLocation)->hasResults()) {
            foreach ($finder as $file) {
                if (strtolower($file->getFilenameWithoutExtension()) === $repositoryName) {
                    if (!empty($this->entity)) {
                        throw new \Exception('Entity can\'t be matched automatically. It looks like there is' .
                            ' more than one ' . $file->getFilenameWithoutExtension() . ' entity. Please use $entity 
                            property on your repository to provide entity you want to use.');
                    }
                    $namespacePart = preg_replace(
                        '#' . $this->defaultEntitiesLocation . '#',
                        '',
                        $file->getPath() . '/' . $file->getFilenameWithoutExtension()
                    );
                    $this->entity = $this->defaultEntitiesNamespace . preg_replace('#/#', '\\', $namespacePart);
                }
            }
        }
    }
}

Ok, so what is happening here? I have bound some values to the container in services.yml:

 services:
        # default configuration for services in *this* file
        _defaults:
            autowire: true      # Automatically injects dependencies in your services.
            autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
            bind:
                $defaultEntitiesLocation: '%kernel.project_dir%/src/Entity'
                $defaultEntitiesNamespace: 'App\Entity'
  1. Then in our new extension class, I know where by default to look for my Entities (this enforces some consistency).

  2. VERY IMPORTANT BIT - I assume that we will name Repositories and Entities with exactly the same so for example: Post will be our Entity and PostRepository is our repository. Just note that the word Repository is not obligatory. If it is there it will be removed.

  3. Some clever logic will create namespaces for you - I assume that you will follow some good practices and that it will all be consistent.

  4. It's done! To have your repository autowired all you need to do is extend your new base repository class and name Entity the same as the repository. so End result looks like this:

    <?php
    
    namespace App\Database\Repository\Post;
    
    use App\Database\Repository\Repository;
    use App\Entity\Blog\Post;
    
    /**
     * Class PostRepository
     * @package App\Database\Repository
     */
    class PostRepository extends Repository
    {
        public function test()
        {
            dd(99999, $this->getEntityName());
        }
    }
    

It is CLEAN, AUTOWIRED, SUPER EASY AND QUICK TO CREATE!


Create the custom repository properly

First, you need to create the repository custom class that extends the default repository from doctrine:

use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository
{
   // your own methods
}

Then you need this annotation in the entity class:

/**
 * @ORM\Entity(repositoryClass="MyDomain\Model\UserRepository")
 */

Then you define the repository in the .yml file:

custom_repository:
        class: MyDomain\Model\UserRepository
        factory: ["@doctrine", getRepository]
        arguments:
          - Acme\FileBundle\Model\File

Make sure that in the definition of your repository class points to your custom repository class and not to Doctrine\ORM\EntityRepository.

Inject custom services into your custom repository:

On your custom repository create custom setters for your services

use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository
{
    protected $paginator;

    public function setPaginator(PaginatorInterface $paginator)
    {
        $this->paginator = $paginator;
    }
}

Then inject them like this:

custom_repository:
        class: MyDomain\Model\UserRepository

        factory: ["@doctrine", getRepository]
        arguments:
          - Acme\FileBundle\Model\File
        calls:
          - [setPaginator, ['@knp_paginator']]

Inject your repository into a service:

my_custom_service:
    class: Acme\FileBundle\Services\CustomService
    arguments:
        - "@custom_repository"