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.
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([ ... ]);
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);
}
<?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;
}
}
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'
);
}
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);
}
}
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.
Top comments (8)
Seems like you've missed the true purpose of this pattern. But first, Doctrine is not based on the Repository pattern. The pattern it's based on is called Data Mapper. This is actually written in Doctrine's documentation. Repositories and entity managers are just parts of this bigger concept.
Now to the Repository pattern. The main purpose of this pattern is to have an extra-layer that would abstract away a particular Active Record ORM from the rest of the application. By doing this we:
::save()
,::delete()
etc.).By returning an Eloquent model instance from
UserRepository::find()
method (instead of, say, DTO) you create the so called leaky abstraction and defeat the general idea behind a repository. That's why, I assume, you don't understand the concept in general.Totally agree. Apart form DTO, this article seems not to state the existence of ValueObjects, RepositoryInterfaces and Facades, which is useful in repository pattern
One example of what Pavel is menttioning is the scenario where your application needs to introduce a caching layer when obtaining information, since your code is tied to eloquent models, you have to search every place where the model is queried or persisted to add that cache layer. If a new developer comes and uses the model without considering the caching layer, he or she can create an invalid caching state where the new data is persisted but old data is being presented by the caching layer because the code doesn't prevent you to do this kind of things.
When the business logic depends on a repository this would abstract this logic and the business won't be aware of that cache logic. Maybe some domain entities could be something that makes sense to be stored in a object based storage like a AWS S3, and the repository can just be swapped to work with it and keep the rest of the logic as it is.
The reason of why people use Active Record to persists behind the repository methods is because it's the easiest way to achieve it in many frameworks. Instead, you would have to use the DBAL without the ORM an create your own data mapper (as mentioned by Pavel) to convert the DTO coming from database to the business model that is represented by it. This sounds a lot more harder, and this is because the frameworks aren't usually built to simplify that, not because it's worse.
Other consequence of using Active record models as business models is that they tend to be used as mere DTO to the persistense layer and the business logic is usually (and wrongly implemented in Controllers). In this scenario everything looks fine, until you find repeating business validations across controllers, or when a new entrypoint for your app is added (i.e. cli or queues) you face the fact that you have to move the business logic to somewhere else (the so called services or use cases).
Active record helps for fast prototyping and small applications, but the pattern exists to solve issues usually found in big enterprise architectures.
Functional testing is nice in Laravel and it's quite fast, however, when you need more layers and different persistence mechanism, you will start seeing that your functional tests leak implementation details making the tests harder to maintain and slower. Unit testing becomes a feasible option under these scenarios.
TL; DR; Implementing repositories using active record persistence methods is wrong and it's not the way the repositories should be implemented, and the repository pattern doesn't exists for the sole purpose of mocking during testing but for decoupling the business logic of how the data is persisted as Pavel mentions.
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!
Muy bien post, me dió curiosidad porqué decís que Unit Test no es común en Laravel, podrías ampliar ese concepto?
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.
Quizás eso se resuelve, al menos parcialmente, usando factories, pero sin usar el método create, solo usando "make" no?
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(...)
oUser::all()
.Como son llamadas estáticas, no hay ninguna forma de mockear eso y por tanto, nos hará falta levantar una base de datos.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.