Repository Pattern in Symfony

When I first started working with Symfony I kinda struggled with finding out what is exactly the best practice to create a Repository with Doctrine. As professional developers and practitioners of best practices we learn about all this kinds of laws and patterns we should follow and use. We know that we need to keep our controllers thin. We should decouple our business logic from our presentation and persistence layers. And a Repository (or some other persistence pattern) should be used to interact with our database. This to keep our code separated and clean. But how do we actually start implementing this, is another question. But today we will find out how to do that with Repositories.

Service Locator

When you look into the Symfony documentation. You see that the examples use $this->getDoctrine()->getManager(); and $this->getDoctrine()->getRepository(User::class);. These are auto generated repositories which are called and returned using the Service Locator Pattern.  

The Symfony documentation has good examples for entry-level developers. But when you look closer and let your brain grind over all these best practices and patterns you know. You will see that these examples are not to be used blindly.  As a professional developer you are tasked to apply all your knowledge and not work in the framework by following the examples to the letter. You need to use your knowledge and use the framework to your will. By using and (not blindly) applying the best practices and patterns you learned.

Instead of the Service Locator Pattern we should use Dependency Injection for our Repositories. We are then moving away from hard coding the use of our Repositories, but instead we pass them as arguments (injection). This makes our Repositories more decoupled from the rest of our code.

So together we are going to start from the Service Locator Pattern and move away to using Dependency Injection instead. So lets get started!

Lets get some Doctrine

After you created your Product entity and added some ORM mapping to it you can get a Repository by using the ManagerRegistry. This by using the getDoctrine method provided by the controller. From here we can get a Repository for our Entity and start finding our product and do some changes on it.

class ProductController extends Controller
{
    /**
     * @Route("/product/{productId}", name="product_edit"), requirements={productId: "\d+"}
     */
    public function edit($productId): Response
    {
        // Get our repository
        $repository = $this->getDoctrine()->getRepository(Product::class);

        // Find a product
        $product = $repository->find($productId);
        if ($product) {
            // Change the product
            $product->setName('Product 1');

            // Get the entity manager
            $entityManager = $this->getDoctrine()->getManager();

            // Save the changes to database
            $entityManager->persist($product);
            $entityManager->flush();
        }
        
        ...
    }
}

Its that simple. Now we have access to all the Doctrine’s findBy, findOneBy and findAll methods. For some this is called the power of ORM. For small short lived applications, this might be OK. But not for us. We want to create long living and professional applications. We want to do better then this.

Diving into getDoctrine

So before we continue. What does this getDoctrine method in our controller actually do?

/**
 * Shortcut to return the Doctrine Registry service.
 *
 * @throws \LogicException If DoctrineBundle is not available
 *
 * @final
 */
protected function getDoctrine(): ManagerRegistry
{
    if (!$this->container->has('doctrine')) {
        throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
    }

    return $this->container->get('doctrine');
}

Its a shortcut to get doctrine from the container. This is exactly what we want to avoid.

Service class

Our controllers should be thin and decoupled from business logic. The preferred solution is to use services for this.  So if our business requires to dispatch an event on every product update, we should not dispatch our events from the controller but from our service. If we update our product from a controller, webservice or whatever. It should not matter. We want that event dispatched. Thus, we should avoid any business logic like this in our controller.

So like before we first need to get the ManagerRegistry in here. Instead of getting the ManagerRegistry from our container. We now get it trough DI (Dependency Injection).

final class ProductService
{
    private $managerRegistry;

    public function __construct(ManagerRegistry $managerRegistry)
    {
        $this->managerRegistry = $managerRegistry;
    }

    public function updateProduct(Product $product): void
    {
        // Is the same as $this->getDoctrine()->getManager(), but then with an injected managerRegistry.
        $entityManager = $this->managerRegistry->getManager();


        $entityManager->persist($product);
        $entityManager->flush();
 
        // Dispatch some event on every update
    }
}

But we are not done yet. Now we have a violation of the Law of Demeter. This requires to only call methods one level deeper than the current level. We should not go further then only call methods on the ManagerRegistry.

We only need the ManagerRegistry to get the EntityManager. So instead of injection the ManagerRegistry we should inject the EntityManager.

final class ProductService
{
    /**
     * @var EntityManager
     */
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function updateProduct(Product $product): void
    {
        $this->entityManager->persist($product);
        $this->entityManager->flush(); // Debatable if should be here (for another blog post?)

        // Dispatch some event on every update
    }
}

Get some repositories

But we want a Repository in our service… We just injected the EntityManager just to get a Repository?

final class ProductService
{
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function getSomething(): Product
    {
        $product = $this->entityManager
            ->getRepository(Product::class)
            ->findOneBy(['title' => 'blah']);

    }
}

You might have already guessed it.  We are breaking the Law of Demeter again. We should only call methods one level deeper than the current level. The solution is to inject the Repository in the service.

A Repository class is born

So now we are going to create our custom Repository class.  But as you can geuss, there are multiple solutions. I’m showing 2 simple solutions that you can use in your projects.  The first one is extending the ServiceEntityRepository. This is a base class which creates a Repository for the Entity that you defined in your constructor.  This Repository will now have all the Doctrine methods available.

    final class ProductRepository extends ServiceEntityRepository
{

    public function __construct(ManagerRegistry $registry)
    {
        // The second parameter is the Entity this Repository uses
        parent::__construct($registry, Product::class);
    }

    public function findOneByTitle(string $title): Product
    {
        return $this->findOneBy(['title' => $title]);
    }

    /**
     * Add a simple save method so you don't need to use persist and flush in your service classes
     */
    public function save(Product $product)
    {
        // _em is EntityManager which is DI by the base class
        $this->_em->persist($product);
        $this->_em->flush();
    }
}

We can then inject this Repository in our ProductService.

final class ProductService
{
    private $productRepository;

    public function __construct(ProductRepository $productRepository){
        $this->productRepository = $productRepository;
    }

    public function getSomething(): Product
    {
        $product = $this->productRepository
            ->findOneBy(['title' => 'blah']);

        ...

    }

    public function updateProduct(Product $product): void
    {
        $this->productRepository->save($product);

        // Dispatch some event on every update
    }

}

This is starting to look a lot better. There is no sign from Doctrine anymore. We decoupled our Service from Doctrine and can now use the methods we defined in our custom Repository. But wait! No, the methods that Doctrine provided. Because did you define findOneBy in the repository? Look again. No we didn’t. By extending from the ServiceEntityRepository we are providing all the Doctrine methods to our clients. As followers of best practices and being strict in what we allow our clients to do. We should try to avoid this to happen. A solution would be to create a repository interface and create your own custom methods. But then again, you can still use the Doctrine methods. Maybe there is a better solution?

Repository by composition

Now we are getting into another best practice. Using composition over inheritance. We should always strive to use composition instead of inheritance. Composition over inheritance is a design principle that gives the design higher flexibility. We are essentially decoupling our code from Doctrine’s Repository now.

In our constructor we inject Doctrines EntityManagerInterface.

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

We use the entityManager  to persist or flush. We ask the entityManager to give us back an objectRepository for our Entity. In the background a factory is used to create a new repository. This is the same repository we extended from in the previous solution.

This means we can use all the Doctrine repository methods from the objectRepository and use them in our own repository. If we want we can directly map them one to one. We now have the power to decide the methods that our clients can use and see. We now have greater control over how this repository is used.

final class ProductRepository
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @var ObjectRepository
     */
    private $objectRepository;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->objectRepository = $this->entityManager->getRepository(Product::class);
    }
    
    public function find(int $productId): Product
    {
        $product = $this->objectRepository->find($productId);
        return $product;
    }

    public function findOneByTitle(string $title): Product
    {
        $product = $this->objectRepository
            ->findOneBy(['title' => $title]);
        return $product;
    }

    public function save(Product $product): void
    {
        $this->entityManager->persist($product);
        $this->entityManager->flush();
    }
}

Repository Interfaces

Now we still have one problem. We are in violation of the dependency inversion principle. By following this principle our layers are cleanly separated and our higher level modules do not depend on our persistence layer.

So lets create an interface for our Repository.

interface ProductRepositoryInterface
{
    public function find(int $productId): Product;

    public function findOneByTitle(string $title): Product;

    public function save(Product $product): void;;
}

And then implement it.

final class ProductRepository implements ProductRepositoryInterface
{

Now in our service we can us our repository by depending on the abstraction instead of the concretion.

final class ProductService
{
    private $productRepository;

    public function __construct(ProductRepositoryInterface $productRepository){
        $this->productRepository = $productRepository;
    }

    public function getSomething(): Product
    {
        $product = $this->productRepository->findOneByTitle('blah');
        
        // Do something
    }

    public function updateProduct(Product $product): void
    {
        $this->productRepository->save($product);

        // Dispatch some event on every update
    }
}

Conclusion

By following principles and best practices we make sure our application is saved against code smell. I have shown you 2 solutions of using Repositories in Symfony. Use one of them and create interfaces for your Repositories. But if you want my humble opinion. You should create a Repository by using composition. And definitely when you are creating highly complex business applications. You should take Technical Debt in the back of your mind. The more you are coupled to the framework, the more Technical Debt you carry and this will slow you down in the long run. Spending a bit more time up front by creating your own custom Repositories might cost you a bit more time up front. But you will save Technical Debt and time in the future. Be pragmatic and think first before you start coding.