0Pricing
PHP Academy · Lesson

From Layered to Clean Architecture

Understand why dependencies should point inward.

From Layered to Clean Architecture is a free PHP Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Clean Architecture

You already know the classic three-layer PHP stack: Controller → Service → Repository → Database. It works, but the business logic ends up coupled to Eloquent, Doctrine, the HTTP request, and the framework lifecycle. Clean Architecture flips the dependency direction so your domain knows nothing about infrastructure. The reward: testable use cases, swappable adapters, and a codebase that survives framework upgrades.

The Dependency Rule

The single rule of Clean Architecture: source code dependencies point only inward. Inner circles (entities, use cases) must never reference outer circles (controllers, ORMs, frameworks). At runtime control flows outward via interfaces, but at compile/import time nothing inner imports anything outer.

  • Entities: enterprise rules
  • Use Cases: application rules
  • Adapters: controllers, presenters, gateways
  • Frameworks & Drivers: DB, HTTP, the web

A Coupled Layered Example

Here is the kind of service most PHP apps ship. Notice the domain logic is tangled with Eloquent and the HTTP response. You cannot unit-test the discount rule without a database and a framework.

<?php
class OrderService
{
    public function place(Request $request)
    {
        $user = User::find($request->user_id); // Eloquent
        $total = 0;
        foreach ($request->items as $i) {
            $total += Product::find($i['id'])->price * $i['qty'];
        }
        if ($user->is_vip) {
            $total *= 0.9; // business rule trapped in infra code
        }
        Order::create(['user_id' => $user->id, 'total' => $total]);
        return response()->json(['total' => $total]);
    }
}

Entities: Framework-Free Domain

An entity encodes enterprise-wide rules and depends on nothing. Plain PHP, no annotations, no base class from the ORM. It is fully constructible in a test.

<?php
final class Money
{
    public function __construct(public readonly int $cents) {
        if ($cents < 0) throw new InvalidArgumentException('negative money');
    }
    public function multiply(float $factor): self {
        return new self((int) round($this->cents * $factor));
    }
}

final class Order
{
    /** @param array<int,int> $lineCents */
    public function __construct(private array $lineCents, private bool $vip) {}
    public function total(): Money {
        $sum = array_sum($this->lineCents);
        $money = new Money($sum);
        return $this->vip ? $money->multiply(0.9) : $money;
    }
}

echo (new Order([1000, 2000], true))->total()->cents, PHP_EOL; // 2700

Use Cases Own the Workflow

A use case (interactor) orchestrates entities and talks to the outside world only through interfaces (ports). It receives a request DTO and returns a response DTO — never an HTTP object.

<?php
interface OrderRepository {
    public function save(Order $order): void;
}

final class PlaceOrder
{
    public function __construct(private OrderRepository $orders) {}

    public function execute(array $lineCents, bool $vip): int {
        $order = new Order($lineCents, $vip);
        $this->orders->save($order);
        return $order->total()->cents;
    }
}

The Boundary Is an Interface

The use case declares the OrderRepository interface it needs. The interface lives in the inner circle; the concrete Eloquent/Doctrine implementation lives outside and depends inward. This is the Dependency Inversion Principle applied at an architectural boundary.

Direction of source dependency: EloquentOrderRepository → OrderRepository (interface), never the reverse.

<?php
// Lives in infrastructure layer, points INWARD to the domain interface
final class EloquentOrderRepository implements OrderRepository
{
    public function save(Order $order): void {
        OrderModel::create(['total' => $order->total()->cents]);
    }
}

Testing Without Infrastructure

Because the use case depends on an interface, tests inject a fake. No database, no framework boot — microsecond-fast unit tests that assert pure business behavior.

<?php
final class InMemoryOrders implements OrderRepository {
    public array $saved = [];
    public function save(Order $o): void { $this->saved[] = $o; }
}

$repo = new InMemoryOrders();
$useCase = new PlaceOrder($repo);
$total = $useCase->execute([1000, 2000], true);

assert($total === 2700);
assert(count($repo->saved) === 1);
echo "PASS total=$total saved=" . count($repo->saved) . PHP_EOL;

Controllers Become Thin Adapters

The controller is now an adapter: it translates HTTP into a use-case call and the result into HTTP. It holds no business rules. Swap REST for CLI or a queue worker and the use case is untouched.

<?php
final class OrderController
{
    public function __construct(private PlaceOrder $placeOrder) {}

    public function store(Request $request): JsonResponse {
        $total = $this->placeOrder->execute(
            lineCents: $request->input('lineCents'),
            vip: (bool) $request->input('vip'),
        );
        return new JsonResponse(['total' => $total], 201);
    }
}

Screaming Architecture

Folder structure should scream the domain, not the framework. Avoid Controllers/, Models/ at the top. Organize by capability so a newcomer sees what the app does.

  • src/Ordering/Domain/ — entities, value objects
  • src/Ordering/Application/ — use cases, port interfaces
  • src/Ordering/Infrastructure/ — Eloquent repos, HTTP controllers

Each bounded context is a top-level folder; the framework lives at the edges.

Enforcing the Dependency Rule

Discipline erodes without tooling. Use deptrac or phparkitect in CI to fail the build when Domain imports Infrastructure. The rule becomes a compile-time guarantee rather than a code-review hope.

# deptrac.yaml
deptrac:
  layers:
    - name: Domain
      collectors: [{ type: directory, value: src/.*/Domain/.* }]
    - name: Application
      collectors: [{ type: directory, value: src/.*/Application/.* }]
    - name: Infrastructure
      collectors: [{ type: directory, value: src/.*/Infrastructure/.* }]
  ruleset:
    Domain: []                       # Domain may depend on nothing
    Application: [Domain]
    Infrastructure: [Application, Domain]

Crossing Boundaries with DTOs

To keep entities from leaking outward, data that crosses a boundary travels as a simple DTO, not as an entity or an ORM model. The use case returns a flat structure the adapter can serialize, so the domain object never escapes the core and the outer layer never gains a handle on internal state.

<?php
final class OrderSummary // boundary DTO, no behavior, no domain types
{
    public function __construct(
        public readonly string $orderId,
        public readonly int $totalCents,
    ) {}
}

final class PlaceOrderV2 {
    public function __construct(private OrderRepository $orders) {}
    public function execute(array $lineCents, bool $vip): OrderSummary {
        $order = new Order($lineCents, $vip);
        $this->orders->save($order);
        return new OrderSummary('ord_1', $order->total()->cents);
    }
}

Quick Check

Which dependency direction is allowed under the Dependency Rule?

Recap

You moved from a coupled layered stack to Clean Architecture:

  • The Dependency Rule: source dependencies point only inward.
  • Entities hold enterprise rules in framework-free PHP.
  • Use cases orchestrate via port interfaces, returning DTOs not HTTP.
  • Controllers and ORM repos are outer adapters that depend inward (DIP).
  • Structure should scream the domain, and tools like deptrac enforce the rule in CI.

Frequently asked questions

Is the “From Layered to Clean Architecture” lesson free?

Yes — the full text of “From Layered to Clean Architecture” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.

What will I learn in “From Layered to Clean Architecture”?

Understand why dependencies should point inward. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start PHP Academy?

No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “From Layered to Clean Architecture” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this PHP Academy lesson?

Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. From Layered to Clean Architecture
  2. Ports and Adapters Explained
  3. Use Cases and Application Services
  4. Dependency Inversion in Practice
← Back to PHP Academy