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)