Repository Pattern in Laravel, it's worth?

Víctor Falcón - Mar 30 '21 - - Dev Community

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.

We can work together!

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.

. . . . . . . . . . . . . . . . . .
Terabox Video Player