Repository pattern the SOLID way in Symfony

Saeid Raei
4 min readJul 30, 2022

--

The problem: if you follow Symfony’s documentation for database you’ll face a concept named Repository (Product repository in Symfony’s example) which would be generated by default if you use the make command. this is what your controller looks like after using the Dependency Injection by type hinting the $postRepoository argument.

<?php

namespace App\Controller;

use App\Repository\PostRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class PostController extends AbstractController
{
#[Route('/post/{id}')]
public function show(int $id, PostRepository $postRepository): Response
{
$post = $postRepository->find($id);

return $this->render('post/show.html.twig', [
'title' => $post->getTitle(),
]);
}
}

and here’s what the repository looks like(I’ve removed the unnecessary stuff)

<?php

namespace App\Repository;

use App\Entity\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
*
@extends ServiceEntityRepository<Post>
*
* @method Post|null find($id, $lockMode = null, $lockVersion = null)
*/
class PostRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}
}

The find method that has been used in the controller is defined on the repository’s parent classes.

The problem is that the generated Repository class is the concrete implementation of database-related work using doctrine. let me elaborate more on this.

In our controller’s method with the $postRepository you can run all sorts of queries on database. for example, there is a findBy method in which you can filter your rows with any condition which we haven’t abstracted.
why is this a problem? if we consider the Dependency Inversion principle of the SOLID principles:

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.

we are making our controller depend on the implementation of database-related work instead of an abstraction which violates this principle.

To add to that if we consider the Clean Architecture layers here:

One of the main rules in Clean Architecture is that the inner layers shouldn’t use the outer layers that in our case the controller is using the database layer.

Solutions

Basically whatever we do we should depend on the interface not the implementation , there are two main solutions for this:

  1. you can create your own repository interface and implementation but you still kind of have to have the original repository class in order to work with doctrine. this solution have been already proposed in this post but the problem with it is that there will be two repository implementations in your project , one of them is generated by symfony and the other one that you create ,that can get confusing and not so clean after all.
  2. you can just make the original repository implement your own interface! this way the original repository will continue to act as a normal repository without the need to refactor anything else. there is a side effect in this solution which might even end up in our favor I’ll elaborate on this later.

so lets see what the second implementation will look like , here’s our Interface

<?php
namespace App\Repository;

use App\Entity\Post;

interface PostRepositoryInterface {
public function findById(int $id):?Post;
}

here’s the Repository:

<?php

namespace App\Repository;

use App\Entity\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
*
@extends ServiceEntityRepository<Post>
*
* @method Post|null find($id, $lockMode = null, $lockVersion = null)
*/
class PostRepository extends ServiceEntityRepository implements PostRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}

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

and here’s what our controller looks like

<?php

namespace App\Controller;

use App\Repository\PostRepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class PostController extends AbstractController
{
#[Route('/post/{id}')]
public function show(int $id, PostRepositoryInterface $postRepository): Response
{
$post = $postRepository->findById($id);

return $this->render('post/show.html.twig', [
'title' => $post->getTitle(),
]);
}
}

As you can see now our controller depends on abstraction (PostRepositoryInterface) not the implementation and we still have access to all the available methods of the default Symfony repository INSIDE the repository where those code belong. if you’re wondering how is the concrete repository being provided when we just type hinted the interface, that how Symfony’s auto wire works, it automatically looks for the implementation and since we have only one implementation it wouldn’t need any configuration.

I’m gonna write another article about how to use multiple implementations in different environments in the future. (I wrote it: “Symfony Testing: using Repository pattern without connecting to database” )

The side effect

One problem this solution has is that you can’t have the methods with the same name as the original repository methods unless you implement them with the same signature but if you think about it you really are not supposed to have general-purpose methods in the repository pattern anyway, for example, the findBy method on the original repository takes any sort of condition to filter the results, that’s not really how repository pattern should look like, in repository pattern, you should have an abstract for each use-case so for example we know what kind of indexes we need more articles to come on this.

Thanks a lot for reading, feel free to criticize.

--

--

Saeid Raei
Saeid Raei

Responses (6)