Good practices for Doctrine repositories
Most code examples that can be fount online tend to extend EntityRepository. But it's not the only way to work with Doctrine repositories. Here's one of the possible ways of define the repository differently.
I am mostly using Doctrine with Symfony. Yet, nothing stays in a way of implementing the following examples outside of Symfony.
The default repository
By default, if there's no repository class configured for an entity, it's generated automatically. You can access it by calling ObjectManager::getRepository(Entity::class)
. Under the hood it's a service locator:
1 2 3 4
public function getRepository(string $entityName)
{
return $this->repositoryFactory->getRepository($this, $entityName);
}
It's convenient and easy to use, I have to admit it. I use it every time I have to use PHP in interactive mode. I can quickly call any of the EntityRepository::find*
methods. There also the magic EntityRepository::__call
that allows to find an entity by its attribute eg.: findOneByTitle('foo')
. There are some cons tho. First of all - no code completion and/or suggestions. Another one is that once you change attribute name of an entity, you have to manually search and update all magic calls.
You can extend the EntityRepository
class and add your own methods. This makes it easier to keep usage of magic methods tamed in a single place. It's still possible to pollute the whole codebase, but now you can use your repository as a service.
Handmade repository service
I decided to not extend EntityRepository
in my projects. I also don't define repository class in entity configuration. Thanks to that, a call to ObjectManager::getRepository
will return the generated class. It can be used by Symfony internals (eg.: to provide an entity as controller param). I also create custom repository classes and inject them wherever I need them. Below is an example of such repository:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
final class PostRepository
{
public function __construct(
private readonly EntityManagerInterface $em
) {}
public function getBySlug(UuidInterface $id): Post
{
$post = $this->em->find(Post::class, $id);
if (!$post) {
throw new PostNotFoundException();
}
return $post;
}
}
This repository has clear boundaries of what it can and can't do. This solution also makes is easier to split repositories and use separate classes eg.: for back and front offices. Code completion works, attributes are typed, it's easier to refactor. It will also ease extraction of interfaces once you need them. Nothing holds me back from building my queries using query builder or DQL directly, instead of using EntityManagerInterface::find
. It was just an example.
A note regarding Symfony. As I already mentioned, because I don't define repository class in entity configuration, Doctrine generates the default. It will be used then by Symfony during controller attribute conversion (when you are using SensioFrameworkExtraBundle).
Query functions
Ocramius in his Doctrine Best Practices included a term "Query Functions". I've been searching for a use case for a very long time. Short version: instead of full repositories, you can create an invokable class that's sole purpose is to execute a query. Here's an example from the presentation:
final class UsersThatHaveAMonthlySubscription
{
public function __construct(EntityManagerInterface $em)
{
// ...
}
public function __invoke(): \Traversable
{
// ... INSERT DQL/SQL HELL HERE ...
}
}
How does it work? Instead of injecting a repository, you inject a query function. One class one query. You can add query modifiers (filters).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public function __invoke(PublishedPostsFilter $filter): \Traversable
{
$qb = $this->em->createQueryBuilder();
// ...
if ($filter->after) {
$qb->andWhere('p.publishedAt > :after');
$qb->setParameter('after', $filter->after);
}
// ...
$qb->setMaxResults($filter->limit);
return $qb->getQuery()->getResult();
}
The get and find difference
I think this is another one I learned from Ocramius. Repositories can expose methods prefixed with get
or find
. find
allows null
, get
throws. Simple rule that makes one's life easier.
1 2 3 4 5 6 7 8 9 10
final class PostRepository
{
// ...
public function getPostById(PostId $id): Post {
}
public function findPostBySlug(string $slug): Post|null {
}
}
Summary
The practices I presented above make my work easier than before implementing them. My code is easier to test and easier to manage. I'm not saying that Doctrine repositories are evil. There are use cases for them (like mentioned parameter conversion). In my opinion, minimizing the amount of code I don't control makes my work easier.
- Don't extend the default Doctrine repository.
- Create methods that give you what you need instead of "jack of all trades" methods.
- Split repository into multiple classes if needed.
- Define
find*
methods for nullable or collection queries. - Define
get*
methods for single-record or single-value queries.