DEV Community

Cover image for Simplifying Data Access in Laravel with the Repository Pattern
Naveen Kola
Naveen Kola

Posted on

Simplifying Data Access in Laravel with the Repository Pattern

In software development, maintaining clean and manageable code is essential. One way to achieve this in Laravel is by using the Repository Pattern. This pattern allows you to separate the data access logic from the business logic, making your code more modular, testable, and maintainable. In this blog post, we’ll explore the Repository Pattern in Laravel and provide an implementation example.

What is the Repository Pattern?

The Repository Pattern is a design pattern that mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects. It encapsulates the logic for accessing data sources, such as databases, and provides a clean API for interacting with data.

Benefits of Using the Repository Pattern

  • Separation of Concerns: Keeps your controllers and services clean by separating data access logic.

  • Easier Testing: Facilitates unit testing by allowing you to mock the repository.

  • Code Reusability: Promotes code reuse by centralizing data access logic in repositories.

Understanding Code Structure Without the Repository Pattern

Before diving into the implementation of the Repository Pattern, let’s look at what code structure might look like without it. Here’s an example of how a typical controller might handle data access directly:

Controller Without Repository Pattern

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\YourModel;

class YourModelController extends Controller
{
    public function index()
    {
        $models = YourModel::all();

        return response()->json($models);
    }

    public function store(Request $request)
    {
        $model = YourModel::create($request->all());

        return response()->json($model, 201);
    }

    public function show(int $id)
    {
        $model = YourModel::findOrFail($id);

        return response()->json($model);
    }

    public function update(Request $request, int $id)
    {
        $model = YourModel::findOrFail($id);
        $model->update($request->all());

        return response()->json($model);
    }

    public function destroy(int $id)
    {
        $model = YourModel::findOrFail($id);
        $model->delete();

        return response()->json(null, 204);
    }
}
Enter fullscreen mode Exit fullscreen mode

Problems with This Approach

  • Tightly Coupled Code: The data access logic is tightly coupled with the controller, making it difficult to manage and test.
    Code Duplication: Similar data access logic might be duplicated across multiple controllers.

  • Harder to Maintain: Any change in the data access logic requires changes in multiple places.

  • Difficult Testing: Testing controllers requires setting up the database, making unit tests slower and harder to isolate.

Implementing the Repository Pattern in Laravel
To address these issues, we can use the Repository Pattern. Let’s dive into the implementation of the Repository Pattern in Laravel.

Step 1: Define the Repository Interface
First, we’ll define an interface for our repository. This interface outlines the methods that our repository will implement.

<?php

namespace App\Repository;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

interface EloquentRepositoryInterface
{
    public function save(array $attributes): Model;
    public function update(Model $model, array $attributes): Model;
    public function delete(Model $model): void;
    public function byId(int $id): ?Model;
    public function byQuery(Builder $query): ?Model;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement the Repository
Next, we’ll implement the repository interface. This implementation will handle the data access logic.

<?php

namespace App\Repository;

use App\Exceptions\Repository\InvalidModelHttpException;
use App\Exceptions\Repository\UnableToDeleteHttpException;
use App\Exceptions\Repository\UnableToSaveHttpException;
use App\Exceptions\Repository\UnableToUpdateHttpException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Psr\Log\LoggerInterface;
use Throwable;

abstract class EloquentRepository implements EloquentRepositoryInterface
{
    public function __construct(
        private readonly Model $modelClass,
        private readonly LoggerInterface $logger
    ) {}

    public function save(array $attributes): Model
    {
        try {
            $model = $this->modelClass->newInstance($attributes);
            $model->saveOrFail();

            return $model;
        } catch (Throwable $e) {
            $this->logger->alert(
                'Unable to save model',
                [
                    'errorMessage' => $e,
                    'method' => __METHOD__,
                    'modelClassName' => $this->modelClass::class
                ]
            );

            throw new UnableToSaveHttpException();
        }
    }

    public function update(Model $model, array $attributes): Model
    {
        if ($model::class !== $this->modelClass::class) {
            throw new InvalidModelHttpException();
        }

        try {
            $model->updateOrFail($attributes);
            return $model;
        } catch (Throwable $e) {
            $this->logger->alert(
                'Unable to update model',
                [
                    'errorMessage' => $e,
                    'method' => __METHOD__,
                    'modelClassName' => $this->modelClass::class
                ]
            );

            throw new UnableToUpdateHttpException();
        }
    }

    public function delete(Model $model): void
    {
        if ($model::class !== $this->modelClass::class) {
            throw new InvalidModelHttpException();
        }

        try {
            $model->deleteOrFail();
        } catch (Throwable $e) {
            $this->logger->alert(
                'Unable to delete model',
                [
                    'errorMessage' => $e,
                    'method' => __METHOD__,
                    'modelClassName' => $this->modelClass::class
                ]
            );

            throw new UnableToDeleteHttpException();
        }
    }

    public function byId(int $id): ?Model
    {
        return $this->modelClass->newQuery()->find($id);
    }

    public function byQuery(Builder $query): ?Model
    {
        return $this->modelClass->newQuery()
                  ->setQuery($query->getQuery())->first();
    }
}
Enter fullscreen mode Exit fullscreen mode

Handling Custom Exceptions
To handle errors gracefully, we’ll define custom exceptions for common error scenarios in our repository.

Step 3: Define Custom Exceptions
Let’s define custom exceptions for handling invalid models and data access failures.

<?php

namespace App\Exceptions\Repository;

use App\Exceptions\HttpException;
use Symfony\Component\HttpFoundation\Response;

class InvalidModelHttpException extends HttpException
{
    protected $code = Response::HTTP_INTERNAL_SERVER_ERROR;
    protected $message = 'Internal server error';
}

class UnableToSaveHttpException extends HttpException
{
    protected $code = Response::HTTP_INTERNAL_SERVER_ERROR;
    protected $message = 'Unable to save the model';
}

class UnableToUpdateHttpException extends HttpException
{
    protected $code = Response::HTTP_INTERNAL_SERVER_ERROR;
    protected $message = 'Unable to update the model';
}

class UnableToDeleteHttpException extends HttpException
{
    protected $code = Response::HTTP_INTERNAL_SERVER_ERROR;
    protected $message = 'Unable to delete the model';
}
Enter fullscreen mode Exit fullscreen mode

Using the Repository in Controllers
Now that we have our repository implemented, let’s see how we can use it in controllers.

Step 4: Injecting the Repository into a Controller
In your controller, you can inject the repository via the constructor and use it to handle data access logic.

<?php

namespace App\Http\Controllers;

use App\Repository\EloquentRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use App\Models\YourModel;

class YourModelController extends Controller
{
    public function __construct(
        private EloquentRepositoryInterface $repository
    )
    {}

    public function index(): JsonResponse
    {
        $models = $this->repository->listByQuery(YourModel::query());

        return response()->json($models);
    }

    public function store(Request $request): JsonResponse
    {
        $model = $this->repository->save($request->all());

        return response()->json($model, 201);
    }

    public function show(int $id): JsonResponse
    {
        $model = $this->repository->byId($id);

        return response()->json($model);
    }

    public function update(Request $request, int $id): JsonResponse
    {
        $model = $this->repository->byId($id);
        $updatedModel = $this->repository->update($model, $request->all());

        return response()->json($updatedModel);
    }

    public function destroy(int $id): JsonResponse
    {
        $model = $this->repository->byId($id);
        $this->repository->delete($model);

        return response()->json(null, 204);
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion
Using the Repository Pattern in Laravel can greatly enhance the maintainability and testability of your application. By abstracting the data access logic into repositories, you can keep your codebase clean and modular. You can then inject these repositories into your controllers and services to handle data operations, making your application more robust and easier to manage.

Give it a try in your next Laravel project and experience the benefits firsthand!

Top comments (1)

Collapse
 
snehalkadwe profile image
Snehal Rajeev Moon

Great post! Very well-written and informative.