Clean Arch is an architectural model that helps structure applications in an organized way, separating responsibilities and facilitating code maintenance. In this post, we will explore how to apply Clean Arch in laravel.
Principles of Clean Architecture
Clean Architecture proposes separating the application into well-defined layers:
- Entities: Pure and independent business rules.
- Use Cases: Implement the application logic.
- Interfaces and Adapters: Communication between use cases and infrastructure (HTTP, database, etc.).
- Frameworks & Drivers: Laravel, Eloquent, Controllers and any specific implementation technology.
This approach reduces coupling and facilitates system evolution without impacting the entire structure.
Example
To provide a clear illustration, a simple reservation system will serve as our example.
To provide a clear illustration, a simple reservation system will serve as our example.
Example: The entity represents the business rule for booking and does not depend on framework:
<?php
namespace App\Domain\Entities;
class Booking
{
public function __construct(
public int $userId,
public int $serviceId,
public string $date
) {
// Business rule can be applied here
}
}
Creating the repository interface
The repository defines persistence but does not depend on Eloquent:
<?php
namespace App\Domain\Repositories;
use App\Domain\Entities\Booking;
interface BookingRepositoryInterface
{
public function save(Booking $booking): Booking;
}
Implementing the Create Booking UseCase
The booking creation logic is isolated in a use case:
<?php
namespace App\Application\UseCases;
use App\Domain\Entities\Booking;
use App\Domain\Repositories\BookingRepositoryInterface;
class CreateBooking
{
public function __construct(private BookingRepositoryInterface $repository) {}
public function execute(array $data): Booking
{
if (strtotime(data_get($data, 'date')) < time()) {
throw new \Exception("It is not possible to book for past dates.");
}
$booking = new Booking(data_get($data, 'user_id'), data_get($data, 'service_id'), data_get($data, 'date'));
return $this->repository->save($booking);
}
}
Implementing the Eloquent Repository
The concrete repository implementation using Eloquent:
<?php
namespace App\Infrastructure\Persistence;
use App\Domain\Entities\Booking;
use App\Domain\Repositories\BookingRepositoryInterface;
use App\Models\Booking as BookingModel;
class EloquentBookingRepository implements BookingRepositoryInterface
{
public function save(Booking $booking): Booking
{
$model = BookingModel::create([
'user_id' => $booking->userId,
'service_id' => $booking->serviceId,
'date' => $booking->date,
]);
return new Booking($model->user_id, $model->service_id, $model->date);
}
}
Creating the Controller
The controller uses the use case to handle HTTP requests:
<?php
namespace App\Infrastructure\Http\Controllers;
use App\Application\UseCases\CreateBooking;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use use Illuminate\Validation\ValidationException;
use Throwable;
class BookingController
{
public function __construct(private readonly CreateBooking $createBooking) {}
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'user_id' => 'required|integer',
'service_id' => 'required|integer',
'date' => 'required|date',
]);
try {
$booking = $this->createBooking->execute($data);
return response()->json(['message' => 'Booking successfully created', 'data' => $booking], 201);
} catch (ValidationException $e) {
return response()->json(['error' => $e->errors()], 422);
}catch (Throwable $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}
Connecting Everything in Laravel
To inject the correct repository into the application, configure it in AppServiceProvider:
<?php
use App\Domain\Repositories\BookingRepositoryInterface;
use App\Infrastructure\Persistence\EloquentBookingRepository;
$this->app->bind(BookingRepositoryInterface::class, EloquentBookingRepository::class);
Benefits of Clean Architecture in Laravel
Modular code:
Facilitates technology and framework changes.
Easier testing:
Use cases and entities can be tested in isolation.
Less coupling:
Allows replacing the ORM, framework, or any layer without affecting the entire application.
Conclusion
Applying Clean Arch helps create more organized, modular, and maintainable systems. In this example, we structured a booking module by separating entities, use cases, repositories, and adapters.
By adopting this approach, you gain:
- More structured and scalable projects.
- Easier maintenance and testing.
- Greater flexibility for integrating new technologies.
If you want to digging deeper into Clean Arch and laravel, here are some useful references:
Top comments (3)
While it is a flow that comes close to clean architecture, is not there yet.
The
execute
function should use a DTO as an argument. That way you don't need thedata_get
function retrieve the input.The
Booking
entity is typically identifiable, that is important for the create/modify/delete lifetime cycle.While the business rules should be in the
__construct
method, a more pragmatic solution is to have a factory method that inspects the business rules and returns anErrorsDTO
or aBooking
instance that can be returned to the controller or continue with the flow.So the execute method can look as follows
This will makes the controller a bit more readable.
Now the pedantic bit:
CreateBooking
should be a dependency of thestore
method because it will not be used by any of the other methods of the controller.That makes sense too.
I tried to make it as simple as possible for understanding and to arouse curiosity. But I really liked what you did above.
I understand you can't show everything.
I think DTO's are an essential part of clean architecture. I know there are variations of the directory structure, for me controllers belong to the interface layer.
And when data is transported between layers a DTO should be used because that is the purpose of that object.