How to use Repository with Doctrine as Service in Symfony

This post was updated at February 2021 with fresh know-how.
What is new?

Update YAML configs to PHP and PHP 7.4 syntax.


Dependency injection with autowiring is super easy since Symfony 3.3. Yet on my mentoring I still meet service locators.

Mostly due to traditional registration of Doctrine repositories.

The way out from service locators to repository as service was described by many before and now we put it into Symfony 3.3 context.

This post is follow up to StackOverflow answer to clarify key points and show the sweetest version yet.

The person who kicked me to do this post was Václav Keberdle - thank you for that.

Clean, Reusable, Independent and SOLID Goal

Our goal is to have clean code using constructor injection, composition over inheritance and dependency inversion principles.

With as simple registration as:

// app/config/services.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();

    $services->defaults()
        ->autowire();

    $services->load('App\\Repository\\', __DIR__ . '/../src/Repository');
};

Nothing more, nothing less. Today, we'll try to get there.

How do we Register Repositories Now?

1. Entity Repository

namespace App\Repository;

use App\Entity\Post;
use Doctrine\ORM\EntityRepository;

final class PostRepository extends EntityRepository
{
    /**
     * Our custom method
     * @return Post[]
     */
    public function findPostsByAuthor(int $authorId): array
    {
        return $this->findBy([
            'author' => $authorId
        ]);
    }
}

Advantages

Disadvantages

namespace App\Repository;

use App\Sorter\PostSorter;
use Doctrine\ORM\EntityRepository;

final class PostRepository extends EntityRepository
{
    public function __construct(PostSorter $postSorter)
    {
        $this->postSorter = $postSorter;
    }
}
// param should be "int", but whatever passes
$this->postRepository->find('someString');
$post = $this->postRepository->find(1);
// some object?
$post->whatMethods()!

2. Entity

namespace App\Entity;

use Doctrine\ORM\Entity;
use Doctrine\ORM\EntityRepository;

/**
 * @Entity(repositoryClass="App\Repository\PostRepository")
 */
final class Post
{
    ...
}

This is a code smell of circular dependency. Why should entity know about its repository?

Static Service Locator Code Smell

Do you know why we need repositoryClass="PostRepository"? It's form of static service locator inside Doctrine:

$this->entityManager->getRepository(Post::class);

It basically works like this:

Instead of registration to Symfony container like any other service, here is uses logic coupled to annotation of specific class. Just a reminder: Occam's razor.


Advantages

Disadvantages


3. Use in Controller

You have to use this complicated service registration in YAML:

// app/config/services.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

return function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();

    $services->defaults()
        ->autowire();

    $services->set('app.post_repository', \Doctrine\ORM\EntityRepository::class)
        ->factory([service('@doctrine.orm.default_entity_manager'), 'getRepository'])
        ->args(['App\Entity\Post']);
};

...or just pass EntityManager.

namespace App\Controller;

use App\Entity\Post;
use App\Repository\PostRepository;
use Doctrine\ORM\EntityManagerInterface;

final class PostController
{
    private PostRepository $postRepository;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->postRepository = $entityManager->getRepository(Post::class);
    }
}

Advantages

Disadvantages

$postRepository = $entityManager->getRepository(Post::class);
// object?
$postRepository->...?;

$post = $this->postRepository->find(1);
// object?
$post->...?;
/** @var App\Entity\Post $post */
$post = $this->postRepository->find(1);
$post->getName();

Advantages Summary

Disadvantages Summary

How to make this Better with Symfony 3.3+ and Composition?

It require few steps, but all builds on single one change. Have you heard about composition over inheritance?

 namespace App\Repository;

 use App\Entity\Post;
+use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\EntityRepository;

-final class PostRepository extends EntityRepository
+final class PostRepository
 {
+    private EntityRepository $repository;
+
+    public function __construct(EntityManagerInterface $entityManager)
+    {
+        $this->repository = $entityManager->getRepository(Post::class);
+    }
 }

Update entity that is now independent on specific repository:

 <?php declare(strict_types=1);

 namespace App\Entity;

 use Doctrine\ORM\Entity;

 /**
- * @Entity(repositoryClass="App\Repository\PostRepository")
+ * @Entity
  */
 final class Post
 {
     ...
 }

Without this, you'd get a segfault error due to circular reference.

That's all! Now you can program the way which is used in the rest of your application:

And how it influenced our 4 steps?


1. Entity Repository

<?php declare(strict_types=1);

namespace App\Repository;

use App\Entity\Post;
use App\Sorter\PostSorter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;

final class PostRepository
{
    private EntityRepository $repository;

    private PostSorter $postSorter;

    public function __construct(EntityManagerInterface $entityManager, PostSorter $postSorter)
    {
        $this->repository = $entityManager->getRepository(Post::class);
        $this->postSorter = $postSorter;
    }

    public function find(int $id): ?Post
    {
        return $this->repository->find($id);
    }
}

Advantages


2. Entity

namespace App\Entity;

use Doctrine\ORM\Entity;

/**
 * @Entity
 */
class Post
{
    ...
}

Advantages

// app/config/services.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();

    $services->defaults()
        ->autowire();

    $services->set(App\Repository\ProductRepository::class);
    $services->set(App\Repository\ProductRedisRepository::class);
    $services->set(App\Repository\ProductBenchmarkRepository::class);
};

3. Use in Controller

namespace App\Controller;

use App\Repository\PostRepository;

final class PostController
{
    private PostRepository $postRepository;

    public function __construct(PostRepository $postRepository)
    {
        $this->postRepository = $postRepository;
    }
}

Advantages


4. Registration services.php

We have a new extra step - registration of services in application container:

// app/config/services.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $containerConfigurator): void {
    $services = $containerConfigurator->services();

    $services->defaults()
        ->autowire();

    $services->load('App\\Repository\\', __DIR__ . '/../src/Repository');
};

All we needed is to apply composition over inheritance pattern.

Quality Test: How to Add new Repository?

The main goal of all this was to make work with repositories typehinted, safe and reliable for you to use and easy to extend. It also minimized space for error, because strict types and constructor injection now validates much of your code for you.

The answer is now simple: just create repository in App\Repository.

Try the same example with your current approach and let me know in the comments.


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!