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:

php
Copied
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:

php
Copied
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:

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

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

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

  1. Don't extend the default Doctrine repository.
  2. Create methods that give you what you need instead of "jack of all trades" methods.
  3. Split repository into multiple classes if needed.
  4. Define find* methods for nullable or collection queries.
  5. Define get* methods for single-record or single-value queries.

Related posts