The Repository Pattern separates data access logic from business logic, making Laravel applications more testable and flexible. While Eloquent is powerful, coupling your entire application to it creates problems. At ZIRA Software, the Repository Pattern has made our code more maintainable and switching data sources trivial.
Why Repository Pattern?
Problems with Eloquent everywhere:
// Controller tightly coupled to Eloquent
class PostController extends Controller
{
public function index()
{
$posts = Post::with('author')
->whereNotNull('published_at')
->orderBy('published_at', 'desc')
->paginate(15);
return view('posts.index', compact('posts'));
}
}
Issues:
- Business logic mixed with data access
- Hard to test without database
- Can't switch from Eloquent to API/Cache
- Duplicated queries across controllers
- Violates Single Responsibility Principle
Repository Pattern benefits:
- Decouples business logic from data source
- Centralized data access logic
- Easy to test with mocks
- Swap implementations (Eloquent/API/Cache)
- Consistent interface across application
- DRY - no duplicate queries
Basic Implementation
Create Repository Interface
app/Repositories/PostRepositoryInterface.php:
<?php namespace App\Repositories;
interface PostRepositoryInterface
{
public function all();
public function find($id);
public function create(array $data);
public function update($id, array $data);
public function delete($id);
public function paginate($perPage = 15);
}
Implement Repository
app/Repositories/EloquentPostRepository.php:
<?php namespace App\Repositories;
use App\Post;
class EloquentPostRepository implements PostRepositoryInterface
{
protected $model;
public function __construct(Post $model)
{
$this->model = $model;
}
public function all()
{
return $this->model->with('author')
->whereNotNull('published_at')
->orderBy('published_at', 'desc')
->get();
}
public function find($id)
{
return $this->model->with('author', 'tags')
->findOrFail($id);
}
public function create(array $data)
{
return $this->model->create($data);
}
public function update($id, array $data)
{
$post = $this->find($id);
$post->update($data);
return $post;
}
public function delete($id)
{
$post = $this->find($id);
return $post->delete();
}
public function paginate($perPage = 15)
{
return $this->model->with('author')
->whereNotNull('published_at')
->orderBy('published_at', 'desc')
->paginate($perPage);
}
}
Bind Interface to Implementation
app/Providers/RepositoryServiceProvider.php:
<?php namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Repositories\PostRepositoryInterface;
use App\Repositories\EloquentPostRepository;
class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
PostRepositoryInterface::class,
EloquentPostRepository::class
);
}
}
Register in config/app.php:
'providers' => [
// ...
App\Providers\RepositoryServiceProvider::class,
],
Use in Controller
app/Http/Controllers/PostController.php:
<?php namespace App\Http\Controllers;
use App\Repositories\PostRepositoryInterface;
use Illuminate\Http\Request;
class PostController extends Controller
{
protected $posts;
public function __construct(PostRepositoryInterface $posts)
{
$this->posts = $posts;
}
public function index()
{
$posts = $this->posts->paginate(15);
return view('posts.index', compact('posts'));
}
public function show($id)
{
$post = $this->posts->find($id);
return view('posts.show', compact('post'));
}
public function store(Request $request)
{
$this->validate($request, [
'title' => 'required|max:255',
'content' => 'required',
]);
$post = $this->posts->create($request->all());
return redirect()->route('posts.show', $post->id);
}
public function update(Request $request, $id)
{
$post = $this->posts->update($id, $request->all());
return redirect()->route('posts.show', $post->id);
}
public function destroy($id)
{
$this->posts->delete($id);
return redirect()->route('posts.index');
}
}
Advanced Repository
Add Custom Methods
PostRepositoryInterface:
<?php namespace App\Repositories;
interface PostRepositoryInterface
{
public function all();
public function find($id);
public function create(array $data);
public function update($id, array $data);
public function delete($id);
public function paginate($perPage = 15);
// Custom methods
public function getPublished();
public function getByAuthor($authorId);
public function getByTag($tagName);
public function search($query);
public function getPopular($limit = 10);
}
Implementation:
<?php namespace App\Repositories;
use App\Post;
class EloquentPostRepository implements PostRepositoryInterface
{
// ... basic methods ...
public function getPublished()
{
return $this->model
->whereNotNull('published_at')
->where('published_at', '<=', now())
->orderBy('published_at', 'desc')
->get();
}
public function getByAuthor($authorId)
{
return $this->model
->where('author_id', $authorId)
->orderBy('created_at', 'desc')
->get();
}
public function getByTag($tagName)
{
return $this->model
->whereHas('tags', function ($query) use ($tagName) {
$query->where('name', $tagName);
})
->get();
}
public function search($query)
{
return $this->model
->where('title', 'like', "%{$query}%")
->orWhere('content', 'like', "%{$query}%")
->get();
}
public function getPopular($limit = 10)
{
return $this->model
->orderBy('views', 'desc')
->limit($limit)
->get();
}
}
Base Repository
app/Repositories/BaseRepository.php:
<?php namespace App\Repositories;
abstract class BaseRepository
{
protected $model;
public function __construct($model)
{
$this->model = $model;
}
public function all()
{
return $this->model->all();
}
public function find($id)
{
return $this->model->findOrFail($id);
}
public function create(array $data)
{
return $this->model->create($data);
}
public function update($id, array $data)
{
$record = $this->find($id);
$record->update($data);
return $record;
}
public function delete($id)
{
$record = $this->find($id);
return $record->delete();
}
public function paginate($perPage = 15)
{
return $this->model->paginate($perPage);
}
public function where($column, $value)
{
return $this->model->where($column, $value)->get();
}
public function with($relations)
{
return $this->model->with($relations);
}
}
Extend base:
<?php namespace App\Repositories;
use App\Post;
class EloquentPostRepository extends BaseRepository implements PostRepositoryInterface
{
public function __construct(Post $model)
{
parent::__construct($model);
}
// Only implement custom methods
public function getPublished()
{
return $this->model
->whereNotNull('published_at')
->orderBy('published_at', 'desc')
->get();
}
}
Criteria Pattern
app/Repositories/Criteria/Criteria.php:
<?php namespace App\Repositories\Criteria;
interface Criteria
{
public function apply($model);
}
app/Repositories/Criteria/PublishedCriteria.php:
<?php namespace App\Repositories\Criteria;
class PublishedCriteria implements Criteria
{
public function apply($model)
{
return $model->whereNotNull('published_at')
->where('published_at', '<=', now());
}
}
app/Repositories/Criteria/ByAuthorCriteria.php:
<?php namespace App\Repositories\Criteria;
class ByAuthorCriteria implements Criteria
{
protected $authorId;
public function __construct($authorId)
{
$this->authorId = $authorId;
}
public function apply($model)
{
return $model->where('author_id', $this->authorId);
}
}
Update repository:
<?php namespace App\Repositories;
use App\Repositories\Criteria\Criteria;
class EloquentPostRepository implements PostRepositoryInterface
{
protected $model;
protected $criteria = [];
public function __construct($model)
{
$this->model = $model;
}
public function pushCriteria(Criteria $criteria)
{
$this->criteria[] = $criteria;
return $this;
}
public function applyCriteria()
{
foreach ($this->criteria as $criteria) {
$this->model = $criteria->apply($this->model);
}
return $this;
}
public function all()
{
$this->applyCriteria();
return $this->model->get();
}
}
Use criteria:
$posts = $this->posts
->pushCriteria(new PublishedCriteria())
->pushCriteria(new ByAuthorCriteria(1))
->all();
Caching Repository
app/Repositories/CachedPostRepository.php:
<?php namespace App\Repositories;
use Illuminate\Support\Facades\Cache;
class CachedPostRepository implements PostRepositoryInterface
{
protected $repository;
public function __construct(PostRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function all()
{
return Cache::remember('posts.all', 60, function () {
return $this->repository->all();
});
}
public function find($id)
{
return Cache::remember("posts.{$id}", 60, function () use ($id) {
return $this->repository->find($id);
});
}
public function create(array $data)
{
$post = $this->repository->create($data);
Cache::forget('posts.all');
return $post;
}
public function update($id, array $data)
{
$post = $this->repository->update($id, $data);
Cache::forget('posts.all');
Cache::forget("posts.{$id}");
return $post;
}
public function delete($id)
{
$result = $this->repository->delete($id);
Cache::forget('posts.all');
Cache::forget("posts.{$id}");
return $result;
}
public function paginate($perPage = 15)
{
return $this->repository->paginate($perPage);
}
}
Bind cached version:
$this->app->singleton(PostRepositoryInterface::class, function ($app) {
return new CachedPostRepository(
new EloquentPostRepository(new Post())
);
});
API Repository
app/Repositories/ApiPostRepository.php:
<?php namespace App\Repositories;
use GuzzleHttp\Client;
class ApiPostRepository implements PostRepositoryInterface
{
protected $client;
protected $baseUrl;
public function __construct()
{
$this->client = new Client();
$this->baseUrl = config('services.api.url');
}
public function all()
{
$response = $this->client->get("{$this->baseUrl}/posts");
return json_decode($response->getBody(), true);
}
public function find($id)
{
$response = $this->client->get("{$this->baseUrl}/posts/{$id}");
return json_decode($response->getBody(), true);
}
public function create(array $data)
{
$response = $this->client->post("{$this->baseUrl}/posts", [
'json' => $data
]);
return json_decode($response->getBody(), true);
}
public function update($id, array $data)
{
$response = $this->client->put("{$this->baseUrl}/posts/{$id}", [
'json' => $data
]);
return json_decode($response->getBody(), true);
}
public function delete($id)
{
$this->client->delete("{$this->baseUrl}/posts/{$id}");
return true;
}
public function paginate($perPage = 15)
{
$response = $this->client->get("{$this->baseUrl}/posts", [
'query' => ['per_page' => $perPage]
]);
return json_decode($response->getBody(), true);
}
}
Switch between implementations:
// Use Eloquent
$this->app->bind(PostRepositoryInterface::class, EloquentPostRepository::class);
// Use API
$this->app->bind(PostRepositoryInterface::class, ApiPostRepository::class);
Testing Repositories
tests/Unit/PostRepositoryTest.php:
<?php
use App\Post;
use App\Repositories\EloquentPostRepository;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class PostRepositoryTest extends TestCase
{
use DatabaseMigrations;
protected $repository;
public function setUp()
{
parent::setUp();
$this->repository = new EloquentPostRepository(new Post());
}
/** @test */
public function it_creates_a_post()
{
$data = [
'title' => 'Test Post',
'content' => 'Test content',
'author_id' => factory(App\User::class)->create()->id
];
$post = $this->repository->create($data);
$this->assertInstanceOf(Post::class, $post);
$this->assertEquals('Test Post', $post->title);
$this->assertDatabaseHas('posts', ['title' => 'Test Post']);
}
/** @test */
public function it_finds_a_post()
{
$post = factory(Post::class)->create();
$found = $this->repository->find($post->id);
$this->assertEquals($post->id, $found->id);
}
/** @test */
public function it_updates_a_post()
{
$post = factory(Post::class)->create(['title' => 'Original']);
$updated = $this->repository->update($post->id, ['title' => 'Updated']);
$this->assertEquals('Updated', $updated->title);
$this->assertDatabaseHas('posts', ['title' => 'Updated']);
}
/** @test */
public function it_deletes_a_post()
{
$post = factory(Post::class)->create();
$this->repository->delete($post->id);
$this->assertDatabaseMissing('posts', ['id' => $post->id]);
}
}
Mock repository in controller tests:
<?php
use App\Repositories\PostRepositoryInterface;
class PostControllerTest extends TestCase
{
/** @test */
public function it_displays_posts()
{
$repository = $this->mock(PostRepositoryInterface::class);
$repository->shouldReceive('paginate')
->once()
->with(15)
->andReturn(collect([
['id' => 1, 'title' => 'Post 1'],
['id' => 2, 'title' => 'Post 2'],
]));
$response = $this->get('/posts');
$response->assertStatus(200);
$response->assertSee('Post 1');
}
}
Repository Generator
Create artisan command:
<?php namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
class MakeRepository extends Command
{
protected $signature = 'make:repository {name}';
protected $description = 'Create a new repository';
protected $files;
public function __construct(Filesystem $files)
{
parent::__construct();
$this->files = $files;
}
public function handle()
{
$name = $this->argument('name');
$this->createInterface($name);
$this->createRepository($name);
$this->info("Repository {$name} created successfully!");
}
protected function createInterface($name)
{
$stub = $this->files->get(base_path('stubs/repository-interface.stub'));
$stub = str_replace('DummyClass', $name, $stub);
$path = app_path("Repositories/{$name}RepositoryInterface.php");
$this->files->put($path, $stub);
}
protected function createRepository($name)
{
$stub = $this->files->get(base_path('stubs/repository.stub'));
$stub = str_replace('DummyClass', $name, $stub);
$path = app_path("Repositories/Eloquent{$name}Repository.php");
$this->files->put($path, $stub);
}
}
Use:
php artisan make:repository Post
Best Practices
- Keep repositories focused - One model per repository
- Use interfaces - Enables swapping implementations
- Don't expose Eloquent - Return arrays/DTOs if needed
- Use criteria - For reusable query logic
- Cache strategically - Decorator pattern for caching
- Test repositories - Unit test data access logic
- Document methods - Clear return types and parameters
When NOT to Use
- Simple CRUD apps - Eloquent alone is fine
- Prototypes - Adds overhead
- Small teams - May overcomplicate
Use repositories when:
- Application is complex
- Multiple data sources
- Need extensive testing
- Team is large
Conclusion
The Repository Pattern decouples Laravel applications from Eloquent, making code more testable and maintainable. While it adds abstraction, the flexibility gained is worth it for complex applications. At ZIRA Software, repositories have enabled painless transitions between databases and APIs.
Start simple with basic CRUD repositories. Add criteria and caching as needed. Don't over-engineer—use when it solves real problems.
Building complex Laravel applications that need clean architecture? Contact ZIRA Software to discuss design patterns, testing strategies, and scalable application architecture.