Mastering the Repository Pattern in Laravel: Clean Architecture for Better Testability
Introduction
The Repository Pattern is one of the most powerful design patterns for Laravel developers looking to write cleaner, more maintainable code. By abstracting data access logic, it creates a consistent interface between your business logic and data storage, making your applications more testable and flexible.
In this comprehensive guide, we'll explore how to implement the Repository Pattern in Laravel, complete with practical examples and best practices from real-world projects.
Why Use the Repository Pattern?
Before diving into implementation, let's understand why this pattern is valuable:
- Separation of Concerns: Business logic is separated from data access logic
- Testability: Easy to mock repositories for unit testing
- Flexibility: Switch between different data sources without changing business logic
- Code Reusability: Common queries can be centralized and reused
- Maintainability: Changes to data access patterns are isolated
Setting Up the Repository Structure
Let's start by creating a basic repository structure for a blog application. First, create the necessary directories:
app/
├── Repositories/
│ ├── Contracts/
│ │ └── PostRepositoryInterface.php
│ └── Eloquent/
│ └── PostRepository.php
└── Services/
└── PostService.phpStep 1: Create the Repository Interface
First, define the contract that our repository will implement:
<?php
namespace App\Repositories\Contracts;
use App\Models\Post;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
interface PostRepositoryInterface
{
public function getAll(): Collection;
public function getPaginated(int $perPage = 15): LengthAwarePaginator;
public function getById(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 getPublished(): Collection;
public function getByCategory(string $category): Collection;
}Step 2: Implement the Eloquent Repository
Now, create the concrete implementation:
<?php
namespace App\Repositories\Eloquent;
use App\Models\Post;
use App\Repositories\Contracts\PostRepositoryInterface;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
class PostRepository implements PostRepositoryInterface
{
protected Post $model;
public function __construct(Post $model)
{
$this->model = $model;
}
public function getAll(): Collection
{
return $this->model->with(['author', 'category'])->get();
}
public function getPaginated(int $perPage = 15): LengthAwarePaginator
{
return $this->model->with(['author', 'category'])
->latest()
->paginate($perPage);
}
public function getById(int $id): ?Post
{
return $this->model->with(['author', 'category'])->find($id);
}
public function create(array $data): Post
{
return $this->model->create($data);
}
public function update(int $id, array $data): bool
{
return $this->model->where('id', $id)->update($data);
}
public function delete(int $id): bool
{
return $this->model->destroy($id);
}
public function getPublished(): Collection
{
return $this->model->where('status', 'published')
->with(['author', 'category'])
->latest()
->get();
}
public function getByCategory(string $category): Collection
{
return $this->model->whereHas('category', function ($query) use ($category) {
$query->where('slug', $category);
})
->where('status', 'published')
->with(['author', 'category'])
->latest()
->get();
}
}Step 3: Register in Service Container
Bind the interface to the implementation in AppServiceProvider:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Repositories\Contracts\PostRepositoryInterface;
use App\Repositories\Eloquent\PostRepository;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
PostRepositoryInterface::class,
PostRepository::class
);
}
}Creating a Service Layer
To further improve separation of concerns, create a service layer that handles business logic:
<?php
namespace App\Services;
use App\Repositories\Contracts\PostRepositoryInterface;
use App\Models\Post;
use Illuminate\Support\Str;
class PostService
{
protected PostRepositoryInterface $postRepository;
public function __construct(PostRepositoryInterface $postRepository)
{
$this->postRepository = $postRepository;
}
public function createPost(array $data): Post
{
// Business logic: Generate slug from title
$data['slug'] = Str::slug($data['title']);
// Business logic: Set author to current user
$data['author_id'] = auth()->id();
// Business logic: Set default status
$data['status'] = $data['status'] ?? 'draft';
return $this->postRepository->create($data);
}
public function updatePost(int $id, array $data): bool
{
if (isset($data['title'])) {
$data['slug'] = Str::slug($data['title']);
}
return $this->postRepository->update($id, $data);
}
public function getPublishedPostsByCategory(string $category)
{
return $this->postRepository->getByCategory($category);
}
}Using Repositories in Controllers
Now your controllers become much cleaner:
<?php
namespace App\Http\Controllers;
use App\Services\PostService;
use App\Http\Requests\StorePostRequest;
use Illuminate\Http\JsonResponse;
class PostController extends Controller
{
protected PostService $postService;
public function __construct(PostService $postService)
{
$this->postService = $postService;
}
public function index(): JsonResponse
{
$posts = $this->postService->getPublishedPostsByCategory('technology');
return response()->json($posts);
}
public function store(StorePostRequest $request): JsonResponse
{
$post = $this->postService->createPost($request->validated());
return response()->json($post, 201);
}
}Testing with Repositories
The Repository Pattern makes testing much easier. Here's a simple test example:
<?php
use Tests\TestCase;
use App\Services\PostService;
use App\Repositories\Contracts\PostRepositoryInterface;
use App\Models\Post;
use Mockery;
class PostServiceTest extends TestCase
{
public function test_create_post_generates_slug()
{
// Arrange
$mockRepository = Mockery::mock(PostRepositoryInterface::class);
$service = new PostService($mockRepository);
$mockRepository->shouldReceive('create')
->once()
->with(Mockery::on(function ($data) {
return $data['slug'] === 'test-post-title';
}))
->andReturn(new Post());
// Act
$result = $service->createPost(['title' => 'Test Post Title']);
// Assert
$this->assertInstanceOf(Post::class, $result);
}
}Best Practices and Tips
- Keep interfaces focused: Don't create massive interfaces with dozens of methods
- Use meaningful method names:
getPublishedPostsByAuthor()is better thangetPostsBy() - Handle exceptions properly: Let repositories throw exceptions and handle them in services
- Cache at the repository level: Implement caching within repositories for better performance
- Consider using base repositories: Create abstract base classes for common CRUD operations
Conclusion
The Repository Pattern is an invaluable tool for Laravel developers seeking cleaner, more maintainable code. By separating data access from business logic, you create applications that are easier to test, modify, and scale. While it adds some initial complexity, the long-term benefits in code quality and maintainability make it worthwhile for most professional projects.
Start implementing this pattern in your next Laravel project, and you'll quickly see improvements in code organization and testability.