Laravel is a robust and flexible framework, but without proper structure, controllers can easily become bloated with logic. To address this, many developers adopt a modular approach using the Repository Pattern and Services. This article explores these concepts with practical examples to help you build a clean, testable, and maintainable Laravel project.
What is the Repository Pattern ?
The Repository Pattern provides an abstraction layer between the database and the business logic of your application. Instead of directly interacting with Eloquent models in controllers or services, you use repositories to handle data access.
Advantages of the Repository Pattern
- Separation of Concerns: Isolates data access logic from business logic.
- Testability: Repositories can be mocked for testing.
- Reusability: Repository methods can be reused across different parts of the application.
Repository Structure
Repository Interface
Each repository is based on an interface (contract) that defines its methods. Here's an example of a BaseContract
for basic CRUD operations:
<?php
declare(strict_types=1);
namespace App\Repositories\Contract;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
interface BaseContract
{
/**
* Find resource.
*
* @param int $id
* @return Model|null
*/
public function find(int $id): ?Model;
/**
* Find all resources.
*
* @return Collection
*/
public function findAll(): Collection;
/**
* Create new resource.
*
* @param array $data
* @return Model
*/
public function create(array $data): Model;
/**
* Update existing resource.
*
* @param int $id
* @param array $data
* @return Model
*/
public function update(int $id, array $data): Model;
/**
* Delete existing resource.
*
* @param int $id
* @return bool
*/
public function delete(int $id): bool;
}
BaseRepository: A Generic Implementation
The BaseRepository implements the interface and provides generic CRUD functionality:
<?php
namespace App\Repositories;
use App\Repositories\Contract\BaseContract;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
abstract class BaseRepository implements BaseContract
{
public function __construct(
protected Model $model
)
{
}
public function find(int $id): ?Model
{
return $this->model->find($id);
}
public function findAll(): Collection
{
return $this->model->get();
}
public function create(array $data): Model
{
return $this->model->create($data);
}
public function update(int $id, array $data): Model
{
return $this->model
->where('id', $id)
->update($data);
}
public function delete(int $id): bool
{
return $this->model->delete($id);
}
}
Specific Repository
A specific repository, like PostRepository
, inherits from BaseRepository
:
<?php
namespace App\Repositories;
use App\Models\Post;
use App\Repositories\Contract\PostContract;
final class PostRepository extends BaseRepository implements PostContract
{
public function __construct(
protected Post $post
)
{
parent::__construct($this->post);
}
public function getPublishedPosts(): Collection
{
return $this->model->where('is_published', true)->get();
}
}
Services: The Business Logic Layer
Services encapsulate business logic, acting as a bridge between controllers and repositories. They manage complex rules and orchestrations.
Service Example
Hereβs a PostService
that leverages the PostRepository
:
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Post;
use App\Services\Contract\PostContract;
use App\Repositories\Contract\PostContract as PostRepositoryContract;
use Illuminate\Database\Eloquent\Collection;
final class PostService implements PostContract
{
public function __construct(
protected PostRepositoryContract $postRepository
)
{
}
public function get(int $id): ?Post
{
return $this->postRepository->find($id);
}
public function getAll(): Collection
{
return $this->postRepository->findAll();
}
public function create(array $data = []): Post
{
return $this->postRepository->create($data);
}
public function update(int $id, array $data = []): Post
{
return $this->postRepository->update($id, $data);
}
public function delete(int $id): bool
{
return $this->postRepository->delete($id);
}
}
Injecting Services into a Controller
Controllers benefit from dependency injection by using services. This keeps controllers lightweight and focused on request handling.
Controller Example
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Services\PostService;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
final class HomeController extends Controller
{
public function __construct(
protected PostService $postService
) {}
public function index(): Factory|View
{
$posts = $this->postService->getAll();
return view('home', compact('posts'));
}
public function store(): RedirectResponse
{
$data = [
'title' => 'A New Post',
'content' => 'This is the content of the post.',
];
$this->postService->create($data);
return redirect()->route('home')->with('success', 'Post created successfully');
}
public function delete(int $id): RedirectResponse
{
if ($this->postService->delete($id)) {
return redirect()->route('home')->with('success', 'Post deleted');
}
return redirect()->route('home')->with('error', 'Failed to delete post');
}
}
Β Why This Structure?
1. Clear Separation of Concerns
- Controllers manage requests and responses.
- Services handle business logic.
- Repositories handle database access.
2. Ease of Maintenance
- Business logic changes occur in services.
- Database-specific changes occur in repositories.
3. Testability
- Services and repositories can be tested in isolation.
- Controllers can be tested with mocked services.
Conclusion
By combining the Repository Pattern and Services, you create a clean and modular architecture for Laravel projects. Each layer has a well-defined responsibility, improving code readability, maintainability, and testability.
How do you structure your Laravel projects? Do you use these concepts, or do you have other strategies to keep your code clean? Share your thoughts in the comments! π
Top comments (0)