Laravel: Repository Pattern

Laravel and Eloquent offer a straightforward and powerful approach to database interaction, enabling easy data retrieval, saving, updating, and deletion from various points in an application. This simplicity, while beneficial, brings with it a significant challenge: it becomes all too easy to produce code that is tightly coupled and disorganized. This issue is particularly evident when examining the structure of a basic controller, where the convenience of direct database operations can inadvertently lead to a lack of separation of concerns and an increase in code complexity. Fortunately, there is a good solution for that.

Direct Model Usage


Let’s explore a simple controller example to illustrate this point further. It has only one method, it is very similar to things you can see on official documentation – just direct model usage, mix of all layers in just one file:

namespace App\Modules\MyModule\Http\Controllers;

use App\Modules\MyModule\Models\MyModel;
use Illuminate\Http\Request;

class MyController 
{
    public function __invoke($request): JsonResponse
    {
        $items = MyModel::where('foo', '=', $request->bar)->get();
        
        return response()->json([
            'items' => $items
        ]);
    }
}

This example, despite its brevity, presents several issues: the absence of a separate request handling, a lack of a valid response using Resource, and no dedicated action to manage operations. Even if we reorganize this into a more appropriate structure, the problem with the query persists due to its tight coupling with MyModel. A second concern arises if there is a need to reuse the same query elsewhere; any change in business logic would necessitate modifications in multiple locations. This approach is highly inefficient and prone to errors, highlighting the importance of a more decoupled and manageable codebase to facilitate maintenance and scalability.

Rescue: Repository Pattern

The repository pattern offers a compelling solution to the issues mentioned. By abstracting direct model access behind a repository class that encapsulates all necessary methods, we ensure a singular version of our business logic is used across different parts of the application. This consolidation significantly simplifies any required changes to the business logic, mitigating the risk of widespread modifications.

Further enhancing our architecture, the use of interfaces instead of direct repository instances decouples our code from specific implementations. This abstraction layer means that our code depends only on the interface contract, not on the concrete implementation of the repository. Should there be a need to change the data source—from a database to files, or to a completely different database system—a new class implementing our interface suffices. This change does not require alterations to the consuming code, thereby dramatically reducing coupling and enhancing flexibility.

Creating Repository Interface

We will start by creating new interface inside our module (I assume you use modules).

It should define all required methods:

namespace App\Modules\MyModule\Repositories;

interface MyModuleRepositoryInterface
{
    // return type is optional, interface should not be coupled with our model, in this case Collection is fine
    public function getItemsByFoo($bar): Collection; 
    
    public function find($id): ?MyModel
    
}

Repository Implementation

Upon defining the interface, the next step involves creating a new repository and implementing the specified interface. In this example, the implementation will utilize a standard Laravel Eloquent model. This approach allows us to encapsulate the Eloquent model’s functionality within the repository, ensuring that the rest of the application interacts with the database through a consistent and abstracted interface.

By doing so, we maintain the flexibility and power of Eloquent while adhering to the principles of the repository pattern, which aims to decouple the application’s core logic from the details of data access. This method ensures that any changes in the data access layer – be it a shift in the underlying database technology or a change in the model’s structure – have minimal impact on the broader application, thereby enhancing maintainability and scalability.

namespace App\Modules\MyModule\Repositories;

use App\Modules\MyModule\Models\MyModel;

class MyModuleRepository implements MyModuleRepositoryInterface
{
    public function getItemsByFoo($bar): Collection
    {
        return MyModel::where('foo', '=', $bar)->get();
    }
    
    public function find($id): ?MyModel
    {
        return MyModel::find($id);
    }
    
}

Repository Registering

To integrate the newly created interface and its implementation into the Laravel application, it’s essential to register them within a service provider specifically dedicated to repositories. This repository service provider acts as a bridge, binding the abstract interface to its concrete implementation, ensuring that Laravel’s IoC container can resolve these dependencies throughout the application.

The process involves adding this service provider within the module where it’s needed. For optimal performance and adherence to Laravel’s design principles, utilizing a DeferrableProvider in conjunction with registering the repository as a singleton often represents the best practice.

A DeferrableProvider is advantageous because it delays the loading of the service provider until one of the registered services is actually needed. This approach enhances the application’s performance by not loading unnecessary services during the request lifecycle. Registering the repository as a singleton ensures that only one instance of the repository is created and reused across the application, further optimizing resource utilization and maintaining consistency in how the repository is accessed.

To implement this, you would typically add your service provider to the providers array in the config/app.php file or register it within a specific module’s service provider, depending on your application’s architecture. Inside the service provider, you would use the singleton method to bind the interface to the concrete class, ensuring Laravel knows how to resolve this dependency.

This setup not only promotes a clean separation of concerns but also ensures that your application is prepared for future changes, whether they involve switching out the implementation of the repository or changes in the business logic, without affecting the rest of your application’s code.

namespace App\Modules\MyModule\Providers;

use App\Modules\MyModule\Repositories\MyModuleRepository;
use App\Modules\MyModule\Repositories\MyModuleRepositoryInterface;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

final class RepositoryServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(MyModuleRepositoryInterface::class, MyModuleRepository::class);
    }

    public function provides(): array
    {
        return [MyModuleRepositoryInterface::class];
    }
}

Usage

After all these steps we can finally use our new repository.

App:make Example:

$myModuleRepository = App::make(MyModuleRepositoryInterface::class);
$items = $myModuleRepository->getItemsByFoo($foo);

Controller Example:

Thanks to dependency injection provided by Laravel, we can create our repository just by passing it as additional item in request method argument. It is much cleaner way so definitely preferred.

namespace App\Modules\MyModule\Http\Controllers;

use App\Modules\MyModule\Models\MyModel;
use Illuminate\Http\Request;

class MyController 
{
    public function __invoke(
        Request $request,
        MyModuleRepositoryInterface $myModuleRepository
    ): JsonResponse {
        return response()->json([
            'items' => $myModuleRepository->getItemsByFoo($request->bar)
        ]);
    }
}

Of course this example can be also improved.

For example instead of using json method from response, we can utilize Resources to format data properly. Remember it is only simple example, but the key thing is still value: if business logic will change and we will need different data source, we will have to update only RepositoryServiceProvider inside our module.