DEV Community

Aleson França
Aleson França

Posted on

1

Applying Clean Arch in Laravel

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
xwero profile image
david duymelinck

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 the data_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 an ErrorsDTO or a Booking instance that can be returned to the controller or continue with the flow.

So the execute method can look as follows

public function execute(BookingDTO $data) : ErrorsDTO|BookingDTO
{
    $result = Booking::FromDTO($data);

    if($result instanceof ErrorsDTO) {
        return $result;
    }
    // The save method could return ErrorsDTO or BookingDTO.
   // A alternative is that it returns ErrorsDTO or Booking, 
   // and that the Booking is transformed to BookingDTO here
    return $this->repository->save($result);
}
Enter fullscreen mode Exit fullscreen mode

This will makes the controller a bit more readable.

Now the pedantic bit:

  • the CreateBooking should be a dependency of the store method because it will not be used by any of the other methods of the controller.
  • There should be no reason the input changes after Eloquent saves it. The Input should change when the entity is created. The only thing Eloquent should add is the id on creation.
Collapse
 
aleson-franca profile image
Aleson França

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.

Collapse
 
xwero profile image
david duymelinck

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.