Lessons → Laravel Advanced → Lesson 6

Repository Pattern in Laravel

Laravel
⏱ 28 min read🔥 AdvancedNot completed

The Repository Pattern decouples your data access logic from your business logic. Instead of calling Eloquent directly inside controllers, you talk to a repository interface — making your code testable, swappable, and clean.

Why Use the Repository Pattern?

Without RepositoryWith Repository
Controllers call Eloquent directlyControllers call an interface
Hard to unit-test (DB required)Easy to mock the repository
Switching databases breaks controllersSwap the implementation, not the controller
Duplicated query logic everywhereQueries live in one place

1. Folder Structure

Directory Layout
app/
├── Repositories/
│   ├── Contracts/
│   │   └── UserRepositoryInterface.php
│   └── Eloquent/
│       └── UserRepository.php
├── Http/
│   └── Controllers/
│       └── UserController.php
└── Providers/
    └── RepositoryServiceProvider.php

2. Define the Interface (Contract)

app/Repositories/Contracts/UserRepositoryInterface.php
<?php

namespace App\Repositories\Contracts;

use App\Models\User;
use Illuminate\Pagination\LengthAwarePaginator;

interface UserRepositoryInterface
{
    public function all(): LengthAwarePaginator;
    public function find(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function create(array $data): User;
    public function update(int $id, array $data): bool;
    public function delete(int $id): bool;
}

3. Implement the Repository

app/Repositories/Eloquent/UserRepository.php
<?php

namespace App\Repositories\Eloquent;

use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
use Illuminate\Pagination\LengthAwarePaginator;

class UserRepository implements UserRepositoryInterface
{
    public function __construct(private readonly User $model) {}

    public function all(): LengthAwarePaginator
    {
        return $this->model->orderBy('created_at', 'desc')->paginate(15);
    }

    public function find(int $id): ?User
    {
        return $this->model->find($id);
    }

    public function findByEmail(string $email): ?User
    {
        return $this->model->where('email', $email)->first();
    }

    public function create(array $data): User
    {
        return $this->model->create($data);
    }

    public function update(int $id, array $data): bool
    {
        return $this->model->where('id', $id)->update($data) > 0;
    }

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

4. Bind the Interface in a Service Provider

app/Providers/RepositoryServiceProvider.php
<?php

namespace App\Providers;

use App\Repositories\Contracts\UserRepositoryInterface;
use App\Repositories\Eloquent\UserRepository;
use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
    }
}

Register it in bootstrap/providers.php (Laravel 11+) or config/app.php providers array (Laravel 10):

bootstrap/providers.php (Laravel 11+)
return [
    App\Providers\AppServiceProvider::class,
    App\Providers\RepositoryServiceProvider::class,
];

5. Use the Repository in a Controller

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\Repositories\Contracts\UserRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function __construct(
        private readonly UserRepositoryInterface $users
    ) {}

    public function index(): JsonResponse
    {
        return response()->json($this->users->all());
    }

    public function show(int $id): JsonResponse
    {
        $user = $this->users->find($id);

        if (!$user) {
            return response()->json(['message' => 'Not found'], 404);
        }

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

    public function store(Request $request): JsonResponse
    {
        $data = $request->validate([
            'name'     => 'required|string|max:255',
            'email'    => 'required|email|unique:users',
            'password' => 'required|min:8',
        ]);

        $data['password'] = bcrypt($data['password']);

        $user = $this->users->create($data);

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

    public function update(Request $request, int $id): JsonResponse
    {
        $data = $request->validate([
            'name'  => 'sometimes|string|max:255',
            'email' => 'sometimes|email|unique:users,email,' . $id,
        ]);

        $this->users->update($id, $data);

        return response()->json(['message' => 'Updated successfully']);
    }

    public function destroy(int $id): JsonResponse
    {
        $this->users->delete($id);

        return response()->json(['message' => 'Deleted successfully']);
    }
}

6. Generic Base Repository (Optional)

Avoid repeating CRUD methods across every repository by using a base class:

app/Repositories/Eloquent/BaseRepository.php
<?php

namespace App\Repositories\Eloquent;

use Illuminate\Database\Eloquent\Model;

abstract class BaseRepository
{
    public function __construct(protected readonly Model $model) {}

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

    public function create(array $data): Model
    {
        return $this->model->create($data);
    }

    public function update(int $id, array $data): bool
    {
        return $this->model->where('id', $id)->update($data) > 0;
    }

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

// Now extend it:
class UserRepository extends BaseRepository implements UserRepositoryInterface
{
    public function __construct(User $model)
    {
        parent::__construct($model);
    }

    public function findByEmail(string $email): ?User
    {
        return $this->model->where('email', $email)->first();
    }

    public function all(): LengthAwarePaginator
    {
        return $this->model->orderBy('created_at', 'desc')->paginate(15);
    }
}

7. Testing With a Mocked Repository

The biggest win: your controller tests never touch the database.

tests/Feature/UserControllerTest.php
<?php

namespace Tests\Feature;

use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
use Illuminate\Pagination\LengthAwarePaginator;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    public function test_index_returns_paginated_users(): void
    {
        $paginator = new LengthAwarePaginator(
            items: collect([['id' => 1, 'name' => 'Alice']]),
            total: 1,
            perPage: 15,
        );

        $this->mock(UserRepositoryInterface::class)
             ->shouldReceive('all')
             ->once()
             ->andReturn($paginator);

        $this->getJson('/api/users')
             ->assertOk()
             ->assertJsonCount(1, 'data');
    }

    public function test_show_returns_404_when_not_found(): void
    {
        $this->mock(UserRepositoryInterface::class)
             ->shouldReceive('find')
             ->with(999)
             ->andReturn(null);

        $this->getJson('/api/users/999')
             ->assertNotFound();
    }
}
💡
When to use it? The Repository Pattern shines in larger applications (10+ models, complex queries, or teams). For small CRUD apps, it adds indirection without much benefit — use your judgment.
⚠️
Don't over-abstract. Avoid creating repository methods that just proxy every Eloquent method. Keep repositories focused on the query needs of your application, not a wrapper around every ORM feature.
← Previous Lesson Next Lesson →
🧠

Test your knowledge!

Take the Laravel quiz.

Take Quiz →