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)