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