Have you ever felt your laravel project is getting hard to maintain ?
Fat controller, mixed logic, validations everywhere. That's where clean arch can help !
In this post, i'll show you how to apply Clean Arch in a simple Crud
using laravel with step by step examples and tests.
What is Clean Arch ?
It's a way to organize code based on responsibilities, not technology.
The ideia is to separate your system into layers:
- Domain: Pure Business rules.
- Use Cases(App): They make the logic work and use the entities to solve particular problems.
- Interface Adapters: They change data between the outside and inside parts (Controllers, Repositories, Presenters).
- Infrastructure: Eloquent, Database, external tools
Domain (User) Entity
class User {
public function __construct(
public ?int $id,
public string $name,
public string $email,
) {}
public static function create(string $name, string $email): self {
return new self(null, $name, $email);
}
}
DTOs
class CreateUserDTO {
public function __construct(public string $name, public string $email) {}
}
class UpdateUserDTO {
public function __construct(public int $id, public string $name, public string $email) {}
}
(User) Repository Contract
interface UserRepository {
public function save(User $user): User;
public function find(int $id): ?User;
public function all(): array;
public function delete(int $id): void;
}
Eloquent Implementation
class UserRepository implements UserRepository {
public function save(User $user): User {
$model = $user->id ? UserModel::findOrFail($user->id) : new UserModel();
$model->name = $user->name;
$model->email = $user->email;
$model->save();
return new User($model->id, $model->name, $model->email);
}
// Other methods: find(), all(), delete()
}
Note: this class depends on Eloquent, but the rest of the system does not.
Use Case (Create User)
class CreateUser {
public function __construct(private UserRepository $repo) {}
public function execute(CreateUserDTO $dto): User {
$user = User::create($dto->name, $dto->email);
return $this->repo->save($user);
}
}
Controller
class UserController {
public function store(StoreUserRequest $req, CreateUser $uc) {
$dto = new CreateUserDTO(...$req->only(['name', 'email']));
$user = $uc->execute($dto);
return new UserResource($user);
}
// Other methods: index(), show(), update(), destroy()
}
Validation with FormRequests
class StoreUserRequest extends FormRequest {
public function rules(): array {
return ['name' => 'required', 'email' => 'required|email|unique:users'];
}
}
Output Resource
class UserResource extends JsonResource {
public function toArray($request): array {
return ['id' => $this->id, 'name' => $this->name, 'email' => $this->email];
}
}
Tests
Unit test (Use Case):
it('creates user successfully', function (): void {
$repo = Mockery::mock(UserRepository::class);
$repo->shouldReceive('save')->once()->andReturn(new User(1, 'Name', 'name@ex.com'));
$uc = new CreateUser($repo);
$dto = new CreateUserDTO('Aleson', 'name@ex.com');
$user = $uc->execute($dto);
expect($user->id)->toBe(1);
});
Feature test(API)
it('creates user through API', function (): void {
$this->postJson('/api/users', [
'name' => 'Aleson',
'email' => 'a@ex.com'
])
->assertCreated()
->assertJson(['name' => 'Aleson']);
});
Conclusion
By using just a few more files, you get:
✅ Testable code
✅ Clear separation of concerns
✅ Independence from the framework
✅ Long-term maintainability
If you want to scale your project with confidence, this is a great way to start!
Top comments (0)