DEV Community

loading...
Cover image for Repository Pattern in Laravel, it's worth?

Repository Pattern in Laravel, it's worth?

victoor profile image Víctor Falcón Originally published at victorfalcon.es ・4 min read

The repository pattern consists of adding a layer of classes that are in charge of accessing the data source and obtaining the different data models.

🇪🇸 This post is available in spanish

They have methods such as find, findAll, create or update among others and are very common in frameworks like Symfony but not so much in Laravel.

There are articles, videos and even libraries to implement this pattern in Laravel, but does it make sense?

Video: repository pattern in Laravel (in Spanish 🇪🇸)

In Laravel we use Active Record

In Laravel we have Eloquent ORM and it is based on the Active Record pattern. Otherwise, Doctrine, from Symfony, is based on the repository pattern.

In the active record pattern, each model corresponds to a table in our database and this model itself is our way of accessing this table. We will be able to search, create or update records in the table directly using the model.

<?php

// Get user with id = 1
User::find(1);

// Search users or create a new one
User::all();
User::where('email', '=', 'hola@victorfalcon.es')->first();
User::create([ ... ]);
Enter fullscreen mode Exit fullscreen mode

This has led many people to qualify active record as an anti-pattern. Specifically, it breaks with the Single Responsabily Principle of SOLID, since each model is responsible for both interacting with the database and its relationships and also, being a model that also contains some domain/business logic.

Repository pattern in Laravel

From the beginning, it seems as a bad idea to implement a repository pattern with Eloquent ORM. We are making an active record based library work as a repository based ORM.

But it doesn't matter, we want our Laravel application to have repositories. Let's go for it.

Creating a repository

We are going to create a UserRepository interface and his implementation with loquent:

<?php

namespace App\Repositories;

interface UserRepository
{
    public function all();
    public function create(array  $data);
    public function update(array $data, $id);
    public function delete($id);
    public function find($id);
}
Enter fullscreen mode Exit fullscreen mode
<?php

namespace App\Repositories;

use App\Model\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class EloquentUserRepository implements UserRepository
{
    protected $model;

    public function __construct(User $user)
    {
        $this->model = $user;
    }

    public function all()
    {
        return $this->model->all();
    }

    public function create(array $data)
    {
        return $this->model->create($data);
    }

    public function update(array $data, $id)
    {
        return $this->model->where('id', $id)
            ->update($data);
    }

    public function delete($id)
    {
        return $this->model->destroy($id);
    }

    public function find($id)
    {
        if (null == $user = $this->model->find($id)) {
            throw new ModelNotFoundException("User not found");
        }

        return $user;
    }
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we need to bind our repository with the implementation in a service provider:

public function register()
{
    $this->app->bind(
        'App\Repositories\UserRepositoryInterface',
        'App\Repositories\UserRepository'
    );
}
Enter fullscreen mode Exit fullscreen mode

As we can see now we have encapsulated all the data access in a specific class, and we can stop using the model for this although, in reality, our model still has the same methods and the repository depends directly on it and that it has this inheritance with Eloquent.

And the question is:

Me— Have we gained anything, is there any difference between doing User::all() or doing $this->repository->all()?
Symfony dev— Well, even though we're increasing complexity and duplicating code, we can now mock the repository and run tests without accessing the database, and that's cool!
Me— True, but if that was your problem, you should have said earlier. In Laravel we have some good options to fix that.

Why it has no sense, IMO

Most of the time, when someone rejects active record is because it is not testable. You can't test a simple class without having to mount a database, because there is no way to change or replace the model that is part of our domain code. And this would be a big problem if, in Laravel, it was not so easy to test with the database.

As we see in the following test, Laravel comes prepared to do this kind of automatic testing in a simple and clear way and, with Laravel Sail, we don't even have to worry about Docker containers.

use DatabaseMigrations;

class UserCreatorTest extends TestCase
{
    use DatabaseMigrations;

    private $service;

    protected function setUp(): void
    {
        $this->service = new UserCreator();
    }

    public function test_it_creates_an_user(): void
    {
        $data = [
            'name' => 'Víctor',
            'email' => 'hola@victorfalcon.es',
        ];

        ($this->service)($data);

        $this->assertDatabaseHas('users', $data);
    }
}
Enter fullscreen mode Exit fullscreen mode

In addition, the latest versions of Laravel are even prepared to launch these tests in parallel making them run faster, so time is not an issue.

And finally, these tests bring us much more value than unit tests without infrastructure and, in most cases, even if we do unit tests we will also have to do functional tests with database to make sure that everything goes as expected so, in this case, we just make one test only.

To long; Didn't read

In short, we have to assume that unit tests in Laravel are not common, but doing functional tests is easy and quick. This why we can adopt the active record without any issue.

Discussion (5)

pic
Editor guide
Collapse
arielmejiadev profile image
Ariel Mejia

Muy buen post! felicidades me parece que esta muy bien justificado y explicado el como y porque de ambos patrones y para mi tampoco hace sentido usar repository en Laravel, ademas cabe mencionar que eloquent usa la misma sintaxis para el driver de varios motores distintos de bases de datos, que en la vida real sería el justificante de desacoplar, sin embargo ya Laravel lo hace por nosotros, ademas como mencionas no es necesario testear de forma unitaria a eloquent, hace sentido testear tipo "features" donde interactua eloquent con nosotros, saludos!

Collapse
ajest profile image
Pablo Fumarola

Muy bien post, me dió curiosidad porqué decís que Unit Test no es común en Laravel, podrías ampliar ese concepto?

Collapse
victoor profile image
Víctor Falcón Author

Los tests unitarios no necesitan infraestructura (base de datos) y en Laravel, como no podemos mockear esto, la mayoría de los tests que hacemos suelen lanzar migraciones, etc.

Collapse
ajest profile image
Pablo Fumarola

Quizás eso se resuelve, al menos parcialmente, usando factories, pero sin usar el método create, solo usando "make" no?

Thread Thread
victoor profile image
Víctor Falcón Author

El problema no es usar o no factories. El problema viene cuando intentas probar una función en la que dentro se hace, por ejemplo, User::create(...) o User::all().

Como son llamadas estáticas, no hay ninguna forma de mockear eso y por tanto, nos hará falta levantar una base de datos.