Laravel: Repository Pattern [with Unit Test]

Just started working on a new Laravel project. After the Laravel installation, created some models, and controllers. Then started implementing the APIs according to the project requirements.

After implementing a few API endpoints, noticed that the usage of models(to call database operations) is all over the place. Here are the problems of using Eloquent models directly in your controller-

  • Model-related code will be all over the controllers, so the controllers become large.
  • If something is changed in the model, which requires changes in the query, then it becomes difficult. As, we have to go through the controllers and see for places to change.
  • You might need to write the same Eloquent model code in multiple places.
  • Later if we want to change to another ORM or some other data source, it will be difficult to change.

We can achive a better process of handling database operations by using the Repository Pattern.

NOTES

We will still be using the Eloquent models inside our repository.

The Repository pattern just moves the Eloquent model usage to the repositories. We will use the repository from our controller to perform database operations.

We will get the following benefits from using the Repository pattern-

  • Database query-related code will be separate from our other business logic.
  • It becomes easy to change the query in case there is any change in the database schema, or in the business logic.
  • We can avoid writing duplicate code(related to database operation).
  • If we want to change the data source of a resource, we can do that just by changing the relevant repository.

Prerequisites

Make sure PHP is installed on the machine.
Create a new Laravel project, or use any existing project.
Create a new Model, or use existing Models(of existing project).

Here for example I am creating a Model named Customer, using following command-

php artisan make:model Customer

Here is the Model. we want 2 fields “name” and “email” to be fillable-

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'email'];
}

Here is the migration file-

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->email();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('customers');
    }
};

Step #1: Create Repository

Create 2 directories in the “app” directory-

  • Interfaces – for storing repository interfaces. This can be used to store other interfaces also.
  • Repositories – for storing repository classes.
mkdir app/Interfaces

mkdir app/Repositories

Create a new interface for customer in “app/Interfaces/CustomerRepositoryInterface.php“.

<?php
// app/Interfaces/CustomerRepositoryInterface.php

namespace App\Interfaces;

use Illuminate\Database\Eloquent\Collection;
use App\Models\Customer;

interface CustomerRepositoryInterface
{
    public function getAll(): Collection;
    public function getByFilter(
        array $filter = [],
        string $orderBy = 'id',
        string $orderDirection = 'asc'
    ): Collection;
    public function getById(int $id);
    public function create(array $data): Customer;
    public function update(int $id, array $data): Customer;
    public function delete(int $id);
    public function total(array $fitler = []): int;
}

Create CustomerRepository file in path “app/Repositories/CustomerRepository.php“, and add the code below-

<?php
// app/Repositories/CustomerRepository.php

namespace App\Repositories;

use App\Http\DataTransferObjects\CustomerDto;
use App\Interfaces\CustomerRepositoryInterface;
use App\Models\Customer;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Pipeline;

class CustomerRepository implements CustomerRepositoryInterface
{
    public function __construct(private Customer $model)
    {
    }

    public function getAll(): Collection
    {
        return $this->model->get();
    }

    public function getByFilter(
        array $fitler = [],
        string $orderBy = 'id',
        string $orderDirection = 'asc'
    ): Collection {
        $builderWithFilter = $this->processFilter();

        return $builderWithFilter->orderBy($orderBy, $orderDirection)->get();
    }

    public function getById(int $id): Customer
    {
        return $this->model->findOrFail($id);
    }

    public function delete($id): int
    {
        return $this->model->destroy($id);
    }

    public function create(array $data): Customer
    {
        return $this->model->create([
            'name' => $data['name'],
            'email' => $data['email'],
        ]);
    }

    public function update(int $id, array $data): Customer
    {
        return tap($this->model->find($id))->update([
            'name' => $data['name,'],
            'email' => $data['email'],
        ]);
    }

    public function total(array $fitler = []): int
    {
        $builderWithFilter = $this->processFilter();

        return $builderWithFilter->count();
    }

    private function processFilter(array $filters = []): Builder
    {
        $builderWithFilter = $this->model->newQuery();

        if (Arr::has($filters, 'name')) {
            $builderWithFilter->where('name', Arr::get($filters, 'name'));
        }

        if (Arr::has($filters, 'email')) {
            $builderWithFilter->where('email', Arr::get($filters, 'email'));
        }

        return $builderWithFilter;
    }

    public function isFromLead()
    {
        return $this->model->where('is_from_lead', true);
    }
}

Step #2: Register Service Provider

We need to register the repository. So create a provider named “RepositoryServiceProvider“, using following command-

php artisan make:provider RepositoryServiceProvider

This will create a repository provider file in the “app/Providers” directory. Add the following code in the file-

<?php

namespace App\Providers;

use App\Interfaces\CustomerRepositoryInterface;
use App\Repositories\CustomerRepository;
use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register(): void
    {
        $this->app->bind(CustomerRepositoryInterface::class, CustomerRepository::class);
    }

    /**
     * Bootstrap services.
     */
    public function boot(): void
    {
        //
    }
}

Then we need to register the provider-

In Laravel 11 add the “App\Providers\RepositoryServiceProvider::class” in the “boostrap/providers.php“. It should look like below-

<?php
// This change is for Laravel version 11

// boostrap/providers.php

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\RepositoryServiceProvider::class,
];

For Laravel 10 and older version, add the provider “App\Providers\RepositoryServiceProvider::class” in the “providers” array, in file “config/app.php“.

// This change is for Laravel 10 and below

// config/app.php

'providers' => [
    // Other existing providers
    // ...
    // ...
    App\Providers\RepositoryServiceProvider::class,
];

Step #3: Use Repository in Controller

Create a controller for the customers-

php artisan make:controller CustomerController

Inject the repository in the constructor, then we can setup and save it in a class property $customerRepository

class CustomerController extends Controller
{
    public function __construct(protected CustomerRepositoryInterface $customerRepository){

    }
    
    //...
}

Then we can use the repository to perform database operations from our controller-

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreCustomerRequest;
use App\Http\Requests\UpdateCustomerRequest;
use App\Interfaces\CustomerRepositoryInterface;
use App\Models\Customer;

class CustomerController extends Controller
{
    public function __construct(protected CustomerRepositoryInterface $customerRepository){

    }
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $result = $this->customerRepository->getAll();
        
        return response()->json($result);
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreCustomerRequest $request)
    {
        $result = $this->customerRepository->create($request->toArray());

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

    /**
     * Display the specified resource.
     */
    public function show(Customer $customer)
    {
        $result = $this->customerRepository->getById($customer->id);

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

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateCustomerRequest $request, Customer $customer)
    {
        $result = $this->customerRepository->update($customer->id, $request->toArray());

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

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Customer $customer)
    {
        $result = $this->customerRepository->delete($customer->id);

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

Check Implementation

Run the server using following command-

php artisan serve

Use curl to make request to the customer endpoints-

curl 'http://localhost:8000/customers'

Or you can use any other client like postman to make the request.

You should get output like below, if there are some data in the customer table-

[
    {
        "id": 1,
        "name": "first customer",
        "email": "first@test.com",
        "created_at": "2024-04-13T15:58:28.000000Z",
        "updated_at": "2024-04-13T15:58:30.000000Z"
    },
    {
        "id": 2,
        "name": "second customer",
        "email": "second@test.com",
        "created_at": "2024-04-13T15:58:33.000000Z",
        "updated_at": "2024-04-13T15:58:32.000000Z"
    }
]

Unit Test

Create the test file for customer-

php artisan make:test CustomerTest --unit

Use the following line to create an instance of the repository-

$this->customerRepository = $this->app->make('App\Repositories\CustomerRepository');

We have to create the instance in the “setUp” function, the code would look like below-

<?php

namespace Tests\Unit;

use App\Interfaces\CustomerRepositoryInterface;
use App\Models\Customer;
use App\Repositories\CustomerRepository;
use Illuminate\Foundation\Testing\DatabaseTruncation;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class CustomerTest extends TestCase
{
    // use RefreshDatabase;
    use DatabaseTruncation;
    protected CustomerRepositoryInterface $customerRepository;

    public function setUp(): void
    {
        parent::setUp();

        // Set up any dependencies or perform actions required before each test    
        $this->customerRepository = $this->app->make('App\Repositories\CustomerRepository');
    }

    public function test_customer_getAll(): void
    {
        $this->customerRepository->create([
            'name' => 'customer one',
            'email' => 'customerone@test.com',
        ]);

        $this->customerRepository->create([
            'name' => 'customer two',
            'email' => 'customertwo@test.com',
        ]);


        $customers = $this->customerRepository->getAll();

        $this->assertEquals($customers->count(), 2);

        $this->assertEquals($customers->first()['name'], 'customer one');
    }
}

After creating the instance, we can use the repository to perform operations using the repository.

Run test by using following command-

php artisan test

Tests should run successfully, and we will see our test has passed-

Laravel PHPUnit test output
Laravel PHPUnit test output

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.