Advanced Laravel

Repository Pattern in Laravel

20 min Lesson 3 of 40

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.

Benefits of the Repository Pattern:
  • 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
When NOT to Use Repository Pattern: For simple CRUD applications with straightforward data access, the repository pattern may add unnecessary complexity. Use it when you have complex business logic, need better testability, or might switch data sources in the future.

Basic Repository Implementation

Let's start with a simple repository implementation for a Post model.

Creating a Repository Interface:
// 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;
}
Implementing the Repository:
// 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();
    }
}
Binding the Repository:
// 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,
],
Using the Repository in Controllers:
// 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.

Base Repository Interface:
// 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;
}
Base Repository Implementation:
// 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;
    }
}
Extending Base Repository:
// 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.

Creating Criteria Interface:
// app/Repositories/Criteria/CriteriaInterface.php
namespace App\Repositories\Criteria;

use Illuminate\Database\Eloquent\Builder;

interface CriteriaInterface
{
    public function apply(Builder $query): Builder;
}
Implementing Specific Criteria:
// 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');
    }
}
Enhanced Repository with Criteria Support:
// 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
}
Using Criteria in Controllers:
// 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'));
    }
}
Testing Advantage: With repositories, you can easily mock the repository interface in tests, making your tests faster and more isolated:
// 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: ProductRepositoryInterface with methods for CRUD, filtering by category, price range, and search
  • Implementation: ProductRepository extending BaseRepository
  • 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 customer
  • MinimumAmountCriteria - 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
Key Takeaways:
  • 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