DEV Community

Aleson França
Aleson França

Posted on

Clean Architecture with PHP + Laravel

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

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

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

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

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

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

Validation with FormRequests

class StoreUserRequest extends FormRequest { public function rules(): array { return ['name' => 'required', 'email' => 'required|email|unique:users']; } } 
Enter fullscreen mode Exit fullscreen mode

Output Resource

class UserResource extends JsonResource { public function toArray($request): array { return ['id' => $this->id, 'name' => $this->name, 'email' => $this->email]; } } 
Enter fullscreen mode Exit fullscreen mode

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

Feature test(API)

it('creates user through API', function (): void { $this->postJson('/api/users', [ 'name' => 'Aleson', 'email' => 'a@ex.com' ]) ->assertCreated() ->assertJson(['name' => 'Aleson']); }); 
Enter fullscreen mode Exit fullscreen mode

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)