Bringing the theoretical concepts of Clean Architecture to a modern PHP framework like Laravel can seem challenging due to the natural coupling promoted by Eloquent's Active Record pattern. However, designing a strict separation is highly achievable to guarantee that our business core remains independent of the framework and 100% testable.
To do this, we apply the Dependency RuleGlosarioRegla de DependenciaPrincipio central de la Arquitectura Limpia que establece que las dependencias del código fuente solo pueden apuntar hacia adentro, es decir, hacia las capas que contienen la lógica de negocio y las reglas empresariales (Entidades y Casos de Uso). Ningún componente o clase de un círculo interno puede conocer, importar o hacer referencia directa a elementos ubicados en círculos externos (como frameworks, bases de datos o la interfaz de usuario).Ver término completo →: all source code dependencies must point inwards, isolating Laravel and its database in the system's outer circles. If you need to review the theoretical foundations first, we recommend reading our handbook on Clean Architecture Foundations.
Organizing Layers in the Directory Structure
To structure the layers inside Laravel's app/ folder, we use dedicated namespaces to separate responsibilities:
App/Domain: Contains Entities and contract interfaces. This layer is pure business theory and imports absolutely nothing from Laravel.App/Application: Contains Use Cases (application services) and data transfer objects (DTOs).App/Infrastructure: Contains the concrete implementation of data persistence (like Eloquent), external APIs, and HTTP controllers.
Implementing the Core: Domain Entities & Contracts
The innermost layer is the Domain. Here, no database models or framework imports exist. We define a pure business Entity in PHP:
namespace App\Domain\Entities;
class UserEntity
{
public function __construct(
private ?int $id,
private string $name,
private string $email
) {}
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
}Next, we define the contract that the application will need to search and save users. This contract acts as an interface applying the dependency inversion principle of SOLIDGlosarioSOLIDAcrónimo de cinco principios de diseño orientado a objetos y programación diseñados para hacer que el software sea más comprensible, flexible y mantenible: S - Single Responsibility Principle (Responsabilidad Única) O - Open/Closed Principle (Abierto/Cerrado) L - Liskov Substitution Principle (Sustitución de Liskov) I - Interface Segregation Principle (Segregación de Interfaz) D - Dependency Inversion Principle (Inversión de Dependencias) Ver término completo →:
namespace App\Domain\Repositories;
use App\Domain\Entities\UserEntity;
interface UserRepositoryInterface
{
public function findById(int $id): ?UserEntity;
public function save(UserEntity $user): void;
}Orchestration: Use Cases and DTOs
In the application layer, we build the Use Case. It orchestrates the business flow by calling repositories and interacting with entities. To pass data safely between outer layers and use cases, we implement a simple Data Transfer Object (DTO):
namespace App\Application\DTOs;
class CreateUserDTO
{
public function __construct(
public readonly string $name,
public readonly string $email
) {}
}Now we create the Use Case responsible for registering the user, applying the DRYGlosarioDRY (Don't Repeat Yourself)Principio fundamental de diseño de software formulado por Andy Hunt y Dave Thomas. Establece que toda pieza de conocimiento o lógica debe tener una representación única, no ambigua y definitiva en el sistema para evitar duplicidad y facilitar el mantenimiento.Ver término completo → principle by encapsulating this logic in a single place:
namespace App\Application\UseCases;
use App\Application\DTOs\CreateUserDTO;
use App\Domain\Entities\UserEntity;
use App\Domain\Repositories\UserRepositoryInterface;
class CreateUserUseCase
{
public function __construct(
private readonly UserRepositoryInterface $userRepository
) {}
public function execute(CreateUserDTO $dto): UserEntity
{
$user = new UserEntity(null, $dto->name, $dto->email);
$this->userRepository->save($user);
return $user;
}
}Adapters & Infrastructure: Laravel Models & Repositories
To satisfy Clean Architecture in Laravel, we treat Eloquent models and physical tables as external details of the infrastructure circle:
- Eloquent Models: Models in
app/Modelsbelong to the Frameworks & Drivers layer and connect to the external database. - Concrete Repositories: We create a concrete repository in the infrastructure layer that implements the domain interface. It handles persistence using Eloquent and maps Eloquent models to pure domain entities.
namespace App\Infrastructure\Repositories;
use App\Domain\Repositories\UserRepositoryInterface;
use App\Domain\Entities\UserEntity;
use App\Models\User as EloquentUser;
class EloquentUserRepository implements UserRepositoryInterface
{
public function findById(int $id): ?UserEntity
{
$eloquentUser = EloquentUser::find($id);
if (!$eloquentUser) return null;
return new UserEntity(
$eloquentUser->id,
$eloquentUser->name,
$eloquentUser->email
);
}
public function save(UserEntity $user): void
{
EloquentUser::updateOrCreate(
['id' => $user->getId()],
['name' => $user->getName(), 'email' => $user->getEmail()]
);
}
}Binding Services
To let Laravel automatically inject the correct EloquentUserRepository whenever a class requests UserRepositoryInterface, we bind them in app/Providers/AppServiceProvider.php:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Domain\Repositories\UserRepositoryInterface;
use App\Infrastructure\Repositories\EloquentUserRepository;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);
}
}Entry Points: HTTP Controllers
The controller is part of the outer circle. It receives the request, validates the input, instantiates the DTO, and triggers the Use Case:
namespace App\Infrastructure\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Application\UseCases\CreateUserUseCase;
use App\Application\DTOs\CreateUserDTO;
class UserController extends Controller
{
public function __construct(
private readonly CreateUserUseCase $createUserUseCase
) {}
public function __invoke(Request $request): JsonResponse
{
$request->validate([
'name' => 'required|string',
'email' => 'required|email'
]);
$dto = new CreateUserDTO(
$request->input('name'),
$request->input('email')
);
$user = $this->createUserUseCase->execute($dto);
return response()->json([
'id' => $user->getId(),
'name' => $user->getName(),
'email' => $user->getEmail(),
'message' => 'User registered successfully.'
], 201);
}
}Structuring our PHP applications this way allows us to fully isolate the core business logic from framework and infrastructure choices, ensuring system scalability and making unit testing straightforward.