Inject a repository instead of an entity manager
Matthias Noback
It appears that I didn’t make myself clear while writing about entity managers and manager registries yesterday. People were quick to reply that instead you should inject entity repositories. However, I wasn’t talking about entity repositories here. I was talking about classes that get an EntityManager
injected because they want to call persist()
or flush()
. The point of my previous post was that in those cases you should inject the manager registry, because you don’t know beforehand which entity manager manages the entities you are trying to persist. By injecting a manager registry you also make your code useful in contexts where another Doctrine persistence library is used.
However, most classes, especially those close to the business of the application, are not in need of an entity manager, but of an entity repository. Those classes use an injected entity manager only to retrieve a repository:
use Doctrine\ORM\EntityManager;
class SomeCustomerRelatedClass
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function someMethod()
{
$customer = $this
->entityManager
->getRepository('Acme\CustomerBundle\Entity\Customer')
->findOneBy(...);
...
}
}
The Law of Demeter
The entity manager is only injected to retrieve some other object (the repository) on which actually useful methods are called. This is a violation of the Law of Demeter, which requires to only call methods one level deeper than the current level (i.e. we should not go further than calling methods on the injected EntityManager
). This becomes especially problematic when we write unit tests for this class, because we would have to create nested mock objects.
Factory service
When you notice that a class only uses an entity manager to get a repository from it, you’d better refactor it to get the repository itself injected, instead of the entity manager. With Symfony you can define a service for the repository like this:
services:
acme_customer.customer_repository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.default_entity_manager', getRepository]
arguments:
- Acme\CustomerBundle\Entity\Customer
The class
is a required value, though it doesn’t really matter here. What matters is the factory
key. It instructs the service container to call the getRepository()
method on the entity manager service with the entity class name as the first argument. The result will be returned as the repository service. Read more about service factories in the official documentation of the Dependency Injection component.
Once you have defined such a service for the repository that you need in your class, you can inject the repository service itself, instead of the entity manager. This will help you follow the Law of Demeter, which makes testing easier and will make your code a bit simpler:
use Doctrine\ORM\EntityRepository;
class SomeCustomerRelatedClass
{
private $customerRepository;
public function __construct(EntityRepository $customerRepository)
{
$this->customerRepository = $customerRepository;
}
public function someMethod()
{
$customer = $this->customerRepository->findOneBy(...);
...
}
}
Custom repository classes
There are some remaining design issues. The most important one is: the class above is not entirely honest about its dependencies. It does not need just an EntityRepository
, it needs the entity repository for Customer
entities. So you can make your code much more expressive by making use of custom repository classes. You need to create a repository class which extends EntityRepository
and then you need to mention that class in the metadata of your entity:
namespace Acme\CustomerBundle\Entity;
use Doctrine\ORM\EntityRepository;
/**
* @ORM\Entity(repositoryClass="Acme\CustomerBundle\Entity\CustomerRepository")
*/
class Customer
{
...
}
class CustomerRepository extends EntityRepository
{
...
}
Now you can inject the custom repository, which makes your intentions much clearer and the code more expressive:
use Acme\CustomerBundle\Entity\CustomerRepository;
class SomeCustomerRelatedClass
{
private $customerRepository;
public function __construct(CustomerRepository $customerRepository)
{
$this->customerRepository = $customerRepository;
}
...
}
The next step would be to refactor any call to the generic find()
, findBy()
and findOneBy()
methods, including any call that relies on the magic __call()
method of the EntityRepository
class. Consider this call to the CustomerRepository
class:
$this->customerRepository->findBy(array(
'city' => 'Amsterdam',
'confirmed' -> true
));
Such a query couples your class to the actual mapping of the Customer
entity, because if the mapping changes (e.g. you rename the field confirmed
to isConfirmed
), the code in this class needs to be modified too. You can easily prevent this coupling by introducing a specific method in your custom repository class:
class CustomerRepository extends EntityRepository
{
public function findByCity($city, $confirmed)
{
return $this->customerRepository->findBy(array(
'city' => $city,
'confirmed' => $confirmed
));
}
}
The good thing is that any mapping changes will not ripple through to other parts of the application. They will be limited to the entity class itself and its repository class.
Repository interfaces
One other coupling-related design problem is the fact that the CustomerRepository
is being injected, which extends EntityRepository
. This couples a class using the CustomerRepository
to Doctrine ORM: you would not be able to replace the CustomerRepository
with a repository that extends Doctrine\MongoDB\ODM\DocumentRepository
and works with MongoDB documents, because it already extends Doctrine\ORM\EntityRepository
.
The solution would be to apply the Dependency Inversion Principle here (the “D” of the SOLID design principles). It tells us that we should depend on abstractions, not on concretions. In this case we depend on a concrete thing, namely an entity repository. Instead we should depend on an abstract object repository which returns objects, whether they are persisted as documents, entities, or not persisted at all.
So for every entity repository we inject, we need to define an interface, which abstracts the actual database queries. For example:
interface CustomerRepository
{
/**
* @param string $city
* @param boolean $confirmed
* @return Customer[]
*/
public function findByCity($city, $confirmed);
}
We don’t promise to return an array, nor an ArrayCollection
; we promise to return something traversable (i.e. an array or an object that implements \Traversable
) the values of which are instances of Customer
.
Now we rename our existing CustomerRepository
class to DoctrineORMCustomerRepository
or CustomerEntityRepository
and we make sure it implements the new repository interface:
class DoctrineORMCustomerRepository extends EntityRepository implements CustomerRepository
{
public function findByCity($city, $confirmed)
{
...
}
}
This allows you (or others) to define other customer repositories which don’t extend EntityRepository
, but use some other way of persisting and retrieving Customer
objects.
You may be tempted to make the
CustomerRepository
interface extend theObjectRepository
interface from the Doctrine Common persistence sub-library, but I’d recommend against it, because it reintroduces coupling to Doctrine.
Something to be aware of: resetting closed entity managers
As Christophe Coevoet pointed out in a comment about a related article by Wojciech Sznapka, there is one disadvantage to immediately injecting repositories in your services. In some situations, especially in long running parallel processes, an entity manager sometimes commits a database transaction which causes an exception to be thrown, which finally causes the entity manager to close itself. The assumption then is that there probably is some discrepancy between the data in the database and the entities that are kept in memory. In order to prevent accidental damage being made to the data persisted in the database, a closed entity manager refuses to perform any new operation to the database, and whatever method you call on it, you will always get this exception:
[Doctrine\ORM\ORMException] The EntityManager is closed.
In these situations, when the entity manager is closed, you can reset the entity manager by calling resetManager()
on the ManagerRegistry
(e.g. the doctrine
service). This will cause the reference to the existing entity manager object to be set to null
. The next time an entity manager is requested, a new one will be instantiated, which does not rely on entities currently being kept in memory.
The problem is that repositories that were already injected in services like described above will not be refreshed automatically. They still rely on the closed entity manager. This would be a good case for injecting ManagerRegistry
instances everywhere. However, since most applications don’t suffer from the “closed entity manager” problem, I would not advise to apply this by default. But it’s good to know when you run into this difficult situation.
Other interesting material: criteria
Traditionally a repository is meant to accept “criteria” objects, based on which actual database queries can be made. I read about this also on Benjamen Eberlei’s blog, but I never used this in practice (I will investigate this option of course). I saw there is also native support for this in Doctrine ORM, by means of the matching()
method of the EntityRepository
class. Official documentation on this is missing so if you have suggestions, please let me know.
Suggestion: add a save function to your repository
One last suggestion I’d like to point out here (as mentioned by Wojciech Sznapka in a comment on my previous post) is that you can skip injecting any entity manager at all if you add a save()
method on your repository, which performs persist()
and flush()
operation on the entity manager itself:
class DoctrineORMCustomerRepository extends EntityRepository implements CustomerRepository
{
public function save(Customer $customer)
{
$this->_em->persist($customer);
$this->_em->flush();
}
}
You rarely need an entity manager for something else, so this really settles most issues.