The Repository Pattern in Laravel
I recently came across a blog post titled – Laravel People (Generally) Don't Like Repositories. It reminded me of a project I worked on where the repository pattern was diligently followed to abstract away database interactions. Every single model had a corresponding repository interface and concrete implementation. For example:
UserModel.php
UserRepositoryInterface.php
UserRepository.php
Over time, a few things started becoming problematic.
Duplication
The methods in the base repository class looked like this:
abstract class BaseRepository implements RepositoryInterface
{
public function create(array $data) {}
public function update(array $data, $id) {}
public function delete($id) {}
public function all(array $columns = ['*']) {}
public function with(array $relations) {}
public function paginate($perPage = 50, array $columns = ['*'])
// ...
}
Do the methods look familiar? That's because they're mostly wrapper methods to their corresponding methods on the Eloquent Model
class (actually the Builder
class). While trying to extract away database logic, we were basically duplicating Eloquent's functionality.
Method bloat
Some repositories started getting bloated with very specific queries and long method names.
Say you needed to update values on two models, like Parent
and Child
. Invariably, you would end up with a method on ParentRepository
with a name like UpdateDetailsAndUpdateClassificationOnChild
.
There were dozens of methods like the above on some repositories, many with partially duplicated functionality that was difficult to abstract away.
Coupled to Eloquent
The entire premise of the repository pattern is to be able to easily switch between different data sources. But in our case, all repository functions were returning Eloquent models or an Eloquent collection.
In the end, I didn't see the point of using what essentially was a wrapper around Eloquent.
So, what’s the solution?
Thanks for all the problems, what’s the solution? asks the author on the blog post.
Well, first of all, you wouldn't do one thing from his example:
public function update(
UpdateProductRequest $request,
Product $product,
UpdateVariationsAction $updateVariationsAction
): ProductResource
{
// Don't do this
// <--
$product->name = (string) $request->string('name');
$product->price = $request->int('price');
$product->save();
// -->
$updateVariationsAction->execute(
$product,
$request->validated('variations')
);
return ProductResource::make($product);
}
Product
is being modified and saved directly from the controller. Try not to do this. You can follow a pattern already shown in the example above – Action classes. Here's how it can be refactored.
First we create an UpdateProductAction
class to abstract away the database logic. We also inject the existing UpdateVariationsAction
class into our new Action class.
class UpdateProductAction
{
public function __construct(
private UpdateVariationsAction $updateVariationsAction
) {}
public function execute(Product $product, array $validatedData): Product
{
return DB::transaction(function () use ($product, $validatedData) {
$product->name = (string) $validatedData['name'];
$product->price = $validatedData['price'];
$product->save();
$product->load('variations');
$this->updateVariationsAction->execute(
$product->variations,
$validatedData['variations']
);
return $product;
});
return $product;
}
}
Now the controller's update()
method is much simpler. All it's doing is, passing the model to be updated and the request data to the Action class.
public function update(
UpdateProductRequest $request,
Product $product,
UpdateProductAction $updateProductAction
): ProductResource
{
$updatedProduct = $updateProductAction->execute(
$product,
$request->validated()
);
return ProductResource::make($updatedProduct);
}
This pattern also addresses the comment about testing on the linked blog post. You can test the controller without seeding the database. All you need to do is stub or mock UpdateProductAction
.
Another huge benefit is that you can now hook into the UpdateProductAction
flow outside of an HTTP request. For example, you might want to create an artisan command to manually update a product as an admin task. Previously, you would've had to duplicate the functionality in your artisan command class. But now, you can just call UpdateProductAction
and you're done.
All patterns have their own set of trade-offs – there's no perfect one. You can still use repositories if you're willing to live with the trade-offs mentioned above. I chose to move away from them.
Liked this article? Share it on X
Liked this article? Share it on X