Repository Pattern in Laravel
Understanding the Repository Pattern
The Repository Pattern is a design pattern that creates an abstraction layer between your application's business logic and data access layer. It provides a more object-oriented view of the database and helps keep your code clean, testable, and maintainable.
- Decouples business logic from data access implementation
- Makes code more testable through dependency injection
- Centralizes data access logic in one place
- Makes it easier to switch between different data sources
- Reduces code duplication across controllers and services
- Provides a consistent API for data operations
Basic Repository Implementation
Let's start with a simple repository implementation for a Post model.
// app/Repositories/Contracts/PostRepositoryInterface.php
namespace App\Repositories\Contracts;
use App\Models\Post;
use Illuminate\Database\Eloquent\Collection;
interface PostRepositoryInterface
{
public function all(): Collection;
public function find(int $id): ?Post;
public function create(array $data): Post;
public function update(int $id, array $data): bool;
public function delete(int $id): bool;
public function published(): Collection;
public function findBySlug(string $slug): ?Post;
}
// app/Repositories/PostRepository.php
namespace App\Repositories;
use App\Models\Post;
use App\Repositories\Contracts\PostRepositoryInterface;
use Illuminate\Database\Eloquent\Collection;
class PostRepository implements PostRepositoryInterface
{
protected $model;
public function __construct(Post $model)
{
$this->model = $model;
}
public function all(): Collection
{
return $this->model->all();
}
public function find(int $id): ?Post
{
return $this->model->find($id);
}
public function create(array $data): Post
{
return $this->model->create($data);
}
public function update(int $id, array $data): bool
{
$post = $this->find($id);
if (!$post) {
return false;
}
return $post->update($data);
}
public function delete(int $id): bool
{
$post = $this->find($id);
if (!$post) {
return false;
}
return $post->delete();
}
public function published(): Collection
{
return $this->model->where('status', 'published')
->orderBy('published_at', 'desc')
->get();
}
public function findBySlug(string $slug): ?Post
{
return $this->model->where('slug', $slug)->first();
}
}
// app/Providers/RepositoryServiceProvider.php
namespace App\Providers;
use App\Repositories\Contracts\PostRepositoryInterface;
use App\Repositories\PostRepository;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
PostRepositoryInterface::class,
PostRepository::class
);
}
}
// Don't forget to register in config/app.php
'providers' => [
// ...
App\Providers\RepositoryServiceProvider::class,
],
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Repositories\Contracts\PostRepositoryInterface;
use Illuminate\Http\Request;
class PostController extends Controller
{
protected $postRepository;
public function __construct(PostRepositoryInterface $postRepository)
{
$this->postRepository = $postRepository;
}
public function index()
{
$posts = $this->postRepository->published();
return view('posts.index', compact('posts'));
}
public function show(string $slug)
{
$post = $this->postRepository->findBySlug($slug);
if (!$post) {
abort(404);
}
return view('posts.show', compact('post'));
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
'status' => 'required|in:draft,published',
]);
$post = $this->postRepository->create($validated);
return redirect()
->route('posts.show', $post->slug)
->with('success', 'Post created successfully');
}
public function update(Request $request, int $id)
{
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
'status' => 'required|in:draft,published',
]);
$updated = $this->postRepository->update($id, $validated);
if (!$updated) {
return back()->with('error', 'Post not found');
}
return back()->with('success', 'Post updated successfully');
}
public function destroy(int $id)
{
$deleted = $this->postRepository->delete($id);
if (!$deleted) {
return back()->with('error', 'Post not found');
}
return redirect()
->route('posts.index')
->with('success', 'Post deleted successfully');
}
}
Advanced Repository with Base Class
Create a base repository to reduce code duplication across multiple repositories.
// app/Repositories/Contracts/BaseRepositoryInterface.php
namespace App\Repositories\Contracts;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
interface BaseRepositoryInterface
{
public function all(array $columns = ['*']): Collection;
public function paginate(int $perPage = 15, array $columns = ['*']);
public function find(int $id, array $columns = ['*']): ?Model;
public function findOrFail(int $id, array $columns = ['*']): Model;
public function findBy(string $field, $value, array $columns = ['*']): ?Model;
public function findWhere(array $where, array $columns = ['*']): Collection;
public function create(array $data): Model;
public function update(int $id, array $data): bool;
public function delete(int $id): bool;
public function with(array $relations): self;
public function orderBy(string $column, string $direction = 'asc'): self;
}
// app/Repositories/BaseRepository.php
namespace App\Repositories;
use App\Repositories\Contracts\BaseRepositoryInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
abstract class BaseRepository implements BaseRepositoryInterface
{
protected $model;
protected $query;
public function __construct(Model $model)
{
$this->model = $model;
$this->resetQuery();
}
protected function resetQuery(): void
{
$this->query = $this->model->newQuery();
}
public function all(array $columns = ['*']): Collection
{
$result = $this->query->get($columns);
$this->resetQuery();
return $result;
}
public function paginate(int $perPage = 15, array $columns = ['*'])
{
$result = $this->query->paginate($perPage, $columns);
$this->resetQuery();
return $result;
}
public function find(int $id, array $columns = ['*']): ?Model
{
return $this->model->find($id, $columns);
}
public function findOrFail(int $id, array $columns = ['*']): Model
{
return $this->model->findOrFail($id, $columns);
}
public function findBy(string $field, $value, array $columns = ['*']): ?Model
{
return $this->model->where($field, $value)->first($columns);
}
public function findWhere(array $where, array $columns = ['*']): Collection
{
$query = $this->model->newQuery();
foreach ($where as $field => $value) {
if (is_array($value)) {
[$field, $operator, $val] = $value;
$query->where($field, $operator, $val);
} else {
$query->where($field, $value);
}
}
return $query->get($columns);
}
public function create(array $data): Model
{
return $this->model->create($data);
}
public function update(int $id, array $data): bool
{
$model = $this->find($id);
if (!$model) {
return false;
}
return $model->update($data);
}
public function delete(int $id): bool
{
$model = $this->find($id);
if (!$model) {
return false;
}
return $model->delete();
}
public function with(array $relations): self
{
$this->query->with($relations);
return $this;
}
public function orderBy(string $column, string $direction = 'asc'): self
{
$this->query->orderBy($column, $direction);
return $this;
}
}
// app/Repositories/UserRepository.php
namespace App\Repositories;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
class UserRepository extends BaseRepository
{
public function __construct(User $model)
{
parent::__construct($model);
}
public function findByEmail(string $email): ?User
{
return $this->findBy('email', $email);
}
public function active(): Collection
{
return $this->findWhere(['is_active' => true]);
}
public function verified(): Collection
{
return $this->findWhere([
['email_verified_at', '!=', null]
]);
}
public function admins(): Collection
{
return $this->findWhere(['role' => 'admin']);
}
public function withPosts(): self
{
return $this->with(['posts']);
}
}
Criteria Pattern for Complex Queries
The Criteria pattern allows you to build complex, reusable query conditions that can be applied to repositories.
// app/Repositories/Criteria/CriteriaInterface.php
namespace App\Repositories\Criteria;
use Illuminate\Database\Eloquent\Builder;
interface CriteriaInterface
{
public function apply(Builder $query): Builder;
}
// app/Repositories/Criteria/PublishedCriteria.php
namespace App\Repositories\Criteria;
use Illuminate\Database\Eloquent\Builder;
class PublishedCriteria implements CriteriaInterface
{
public function apply(Builder $query): Builder
{
return $query->where('status', 'published')
->where('published_at', '<=', now());
}
}
// app/Repositories/Criteria/RecentCriteria.php
namespace App\Repositories\Criteria;
use Illuminate\Database\Eloquent\Builder;
class RecentCriteria implements CriteriaInterface
{
protected $days;
public function __construct(int $days = 30)
{
$this->days = $days;
}
public function apply(Builder $query): Builder
{
return $query->where('created_at', '>=', now()->subDays($this->days));
}
}
// app/Repositories/Criteria/PopularCriteria.php
namespace App\Repositories\Criteria;
use Illuminate\Database\Eloquent\Builder;
class PopularCriteria implements CriteriaInterface
{
protected $threshold;
public function __construct(int $threshold = 100)
{
$this->threshold = $threshold;
}
public function apply(Builder $query): Builder
{
return $query->where('views', '>=', $this->threshold)
->orderBy('views', 'desc');
}
}
// app/Repositories/BaseRepository.php (enhanced)
namespace App\Repositories;
use App\Repositories\Contracts\BaseRepositoryInterface;
use App\Repositories\Criteria\CriteriaInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
abstract class BaseRepository implements BaseRepositoryInterface
{
protected $model;
protected $query;
protected $criteria = [];
public function __construct(Model $model)
{
$this->model = $model;
$this->resetQuery();
}
protected function resetQuery(): void
{
$this->query = $this->model->newQuery();
}
public function pushCriteria(CriteriaInterface $criteria): self
{
$this->criteria[] = $criteria;
return $this;
}
public function applyCriteria(): self
{
foreach ($this->criteria as $criteria) {
$this->query = $criteria->apply($this->query);
}
$this->criteria = [];
return $this;
}
public function all(array $columns = ['*']): Collection
{
$this->applyCriteria();
$result = $this->query->get($columns);
$this->resetQuery();
return $result;
}
// ... other methods remain the same
}
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Repositories\Contracts\PostRepositoryInterface;
use App\Repositories\Criteria\PublishedCriteria;
use App\Repositories\Criteria\RecentCriteria;
use App\Repositories\Criteria\PopularCriteria;
class PostController extends Controller
{
protected $postRepository;
public function __construct(PostRepositoryInterface $postRepository)
{
$this->postRepository = $postRepository;
}
public function index()
{
// Get recent published posts
$posts = $this->postRepository
->pushCriteria(new PublishedCriteria())
->pushCriteria(new RecentCriteria(30))
->orderBy('published_at', 'desc')
->paginate(15);
return view('posts.index', compact('posts'));
}
public function popular()
{
// Get popular published posts
$posts = $this->postRepository
->pushCriteria(new PublishedCriteria())
->pushCriteria(new PopularCriteria(500))
->paginate(10);
return view('posts.popular', compact('posts'));
}
}
// In your test
$mockRepo = $this->mock(PostRepositoryInterface::class);
$mockRepo->shouldReceive('published')
->once()
->andReturn(collect([/* test data */]));
Exercise 1: Build a Complete Repository
Create a complete repository system for a Product model with the following:
- Interface:
ProductRepositoryInterfacewith methods for CRUD, filtering by category, price range, and search - Implementation:
ProductRepositoryextendingBaseRepository - Bind the repository in a service provider
- Use it in a controller to list products with pagination
Exercise 2: Implement Criteria Pattern
Create the following criteria for an Order repository:
CompletedCriteria- Orders with status "completed"DateRangeCriteria- Orders within a date range (pass start and end dates to constructor)CustomerCriteria- Orders for a specific customerMinimumAmountCriteria- Orders above a minimum amount
Then use these criteria together to find completed orders for a customer within the last month above $100.
Exercise 3: Test Your Repository
Write PHPUnit tests for your PostRepository:
- Test the
create()method creates a post correctly - Test the
update()method updates existing posts - Test the
published()method only returns published posts - Test the
findBySlug()method finds the correct post - Mock the repository in a controller test
- Repository pattern separates data access from business logic
- Interfaces make your code testable through dependency injection
- Base repositories reduce code duplication
- Criteria pattern allows flexible, reusable query conditions
- Repositories make it easier to switch data sources
- Don't over-engineer simple applications - use when complexity justifies it