Repository Pattern in Laravel
LaravelThe 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 Repository | With Repository |
|---|---|
| Controllers call Eloquent directly | Controllers call an interface |
| Hard to unit-test (DB required) | Easy to mock the repository |
| Switching databases breaks controllers | Swap the implementation, not the controller |
| Duplicated query logic everywhere | Queries live in one place |
1. Folder Structure
Directory Layout
app/
├── Repositories/
│ ├── Contracts/
│ │ └── UserRepositoryInterface.php
│ └── Eloquent/
│ └── UserRepository.php
├── Http/
│ └── Controllers/
│ └── UserController.php
└── Providers/
└── RepositoryServiceProvider.php2. 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.
Test your knowledge!
Take the Laravel quiz.