Repository pattern the SOLID way in symfony

The problem: if you follow symfony’s documentation for database you’ll face a concept named Repository (Product repository in symfony’s example) which it 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 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 sort of queries on database. for example there is a findBy method 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 to depend on the implementation of database related work instead of an abstraction which violates this principle.

To add on into 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 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 future.

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 propose method on 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 .

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store