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:
- We have custom Repository class which extends
Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository
class. - Our custom class has
public string $entity
property on it. 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'
Then in our new extension class, I know where by default to look for my Entities (this enforces some consistency).
VERY IMPORTANT BIT - I assume that we will name Repositories and Entities with exactly the same so for example:
Post
will be our Entity andPostRepository
is our repository. Just note that the wordRepository
is not obligatory. If it is there it will be removed.Some clever logic will create namespaces for you - I assume that you will follow some good practices and that it will all be consistent.
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"