Does the repository pattern make sense in Laravel?
The repository pattern has many advantages when it comes to testing and separating our code from the infrastructure, but does it make sense in Laravel?
The repository pattern consists of adding a layer of classes that is in charge of accessing the data source and obtaining the different data models.
They have methods like find
, findAll
, create
or update
among others and they are very common in frameworks like Symfony but not so much in Laravel.
There are articles, videos, and even libraries for implementing this pattern in Laravel, but does it make sense?
Laravel is Active Record
In Laravel we have Eloquent ORM is based on the Active Record pattern and 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 can search, create or update records in the table using the model directly.
<?php
// Obtener el usuario con id = 1
User::find(1);
// Algunas forma de buscar/crear usuarios
User::all();
User::where('email', '=', 'hola@victorfalcon.es')->first();
User::create([ ... ]);
This has caused many people to classify active record as an anti-pattern. Specifically, it breaks with SOLID's Single Responsibly Principle , since each model is responsible for both interacting with the database and its relationships, and also, being a model, it also contains some domain/business logic.
And I don't know if you agree with this or not, I have my opinion, but what you don't have to EVER do is mix both patterns.
Repository pattern in Laravel
For starters, wanting to apply this pattern in Laravel, with Eloquent ORM already sounds very bad. We are making an active record based library work like a repository based ORM when one has nothing to do with the other.
But hey, we are stubborn, we want our Laravel application to have repositories, let's get to it.
Create our repository
Let's create our own UserRepository
that will look something like this:
<?php
namespace App\Repositories;
interface UserRepositoryInterface
{
public function all();
public function create(array $data);
public function update(array $data, $id);
public function delete($id);
public function find($id);
}
<?php
namespace App\Repositories;
use App\Model\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class UserRepository implements UserRepositoryInterface
{
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;
}
}
And finally we will create a repository service container in which we bind the interface to the repository implementation so that we can then inject where we need it.
public function register()
{
$this->app->bind(
'App\Repositories\UserRepositoryInterface',
'App\Repositories\UserRepository'
);
}
As we can now see, we have encapsulated all the access to the data in a specific class and we can now stop using the model for this, although, in reality, our model continues to have the same methods and the repository depends directly on the model and that it has this inheritance. with Eloquent.
And the question is:
Me — Have we won something? Is there a difference between do
User::all()
or do$this->repository->all()
?Symfony dev — Well, even though we're increasing complexity and, even worse, duplicating code, we can now mock the repository and do tests without accessing the database, and that's cool!
Me — True, but if that was your problem, I should have said it before. That is solvable and we don't have to change Eloquent to achieve it.
Why doesn't it make any sense?
Most of the time someone rejects active record it's because it's not testable , you can't test a simple class without having to set up a database, since there is no way to change or replace the model that is part of our domain code and we need to test.
And this would be a big problem if, in Laravel, it wasn't so easy to do a test with the database.
As we see in the following test, Laravel comes prepared to do this type of automatic tests 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);
}
}
In addition, the latest versions of Laravel are even prepared to launch these tests in parallel, making them run faster and time is not a problem.
And finally, these tests provide us with 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 the database to make sure that everything goes as expected.
Conclusion
In short, we have to assume that unit tests in Laravel are not common, but that doing functional tests is a matter of seconds and therefore, we adopt the facility of active record.