Symfony - inject doctrine repository in service
Asked Answered
N

5

19

according to How to inject a repository into a service in Symfony2? it's like

acme.custom_repository:
    class: Doctrine\ORM\EntityRepository
    factory: ['@doctrine.orm.entity_manager', getRepository]
    arguments:
        - 'Acme\FileBundle\Model\File'

but I get an Exception

Invalid service "acme.custom_repository": class "EntityManager5aa02de170f88_546a8d27f194334ee012bfe64f629947b07e4919__CG__\Doctrine\ORM\EntityManager" does not exist.

How can I do this in Symfony 3.4?

update:

EntityClass is actually a valid class FQCN (also used copy reference on phpstorm to be sure) , just renamed it because a companies name is in it :). updated it anyway.

solution

BlueM's solution works perfectly. In case you are not using autowiring here's the service defintion:

Acme\AcmeBundle\Respository\MyEntityRepository:
    arguments:
        - '@Doctrine\Common\Persistence\ManagerRegistry'
        - Acme\AcmeBundle\Model\MyEntity # '%my_entity_class_parameter%'
Nitrous answered 7/3, 2018 at 18:30 Comment(2)
This may help: #44870090Orthogonal
sadly no... still the same exceptionNitrous
M
29

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..

Merchant answered 8/3, 2018 at 8:12 Comment(3)
You also want to be just a tiny bit careful if you happen to be using multiple entity managers. If the same entity class belongs to more than one manager then you can't predict which manager will end up being used.Omniscient
How can I use the Repository now? Can you extend this answer ?Karafuto
Typical usage: constructor injection. Just define “public function __construct(ExampleRepository $repo) { }” in your code, and the dependency will be resolved without configuration.Merchant
H
6

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"
Haslet answered 7/3, 2018 at 23:8 Comment(0)
C
3

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

Christman answered 7/3, 2018 at 19:35 Comment(0)
D
0

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!

Developing answered 12/4, 2020 at 15:48 Comment(0)
G
0

What about the drawbacks about the ServiceEntityRepository?

https://symfony.com/doc/current/doctrine/multiple_entity_managers.html

One entity can be managed by more than one entity manager. This however results in unexpected behavior when extending from ServiceEntityRepository in your custom repository. The ServiceEntityRepository always uses the configured entity manager for that entity.

In order to fix this situation, extend EntityRepository instead and no longer rely on autowiring:

In an own project I've seen that using:

$repository = $entityManager->getRepository(MyEntity:class)

The $repository->_em is not equals to $entityManager (with both using the same connection), causing problems like:

$entity = $entityManager->getRepository(MyEntity:class)->find($id);
$entityManager->refresh($entity); // throws 'entity is not managed'

That's why the entity is fetched with $repository->_em and the refresh (or persist, flush, etc.) is using $entityManager.

This problem is described here: https://github.com/symfony/symfony-docs/issues/9878

So... You can't rely in ServiceEntityRepository using multiple entity managers, but the EntityRepository doesn't allow autowire, so, what?

My two cents (I believe this should be works in every scenario):

Manually set the class metadata (something like you need to do in the constructor of the ServiceEntityManager), so I can:

Remove the autowire of repositories in services.yaml:

    App\:
        resource: '../src/*'
        exclude: '../src/{Entity,Migrations,Repository,Tests,Kernel.php,Client}'

(you also add the repositories below in services.yaml)

And create another /config/packages/repositories.yaml and add:

my.entity.metadata:
    class: App\Entity\Metadata
    arguments: 
        $entityName: App\Entity\MyEntity

App\Repository\MyEntityRepository:
    arguments:
        [$class: my.entity.metadata]

Now you have a EntityRepository that is capable of being autowireable. You can make a repositories.yaml file in the config and keep updated when you create/edit/delete your repositories. Is not cleanest but it should be works.

Grose answered 23/12, 2022 at 11:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.