Hexagonal Architecture (Ports & Adapters)
debt(d5/e9/b9/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list phpstan, psalm, and deptrac — all specialist static analysis tools. Deptrac in particular is purpose-built to enforce architectural layer boundaries and would catch domain classes importing Doctrine ORM, Eloquent Model, or framework HTTP classes directly, exactly as noted in code_pattern. This is not caught by the compiler or default linters, but is detectable with deliberate specialist tooling.
Closest to 'architectural rework' (e9). The quick_fix states 'Move all infrastructure concerns (DB, HTTP, email) behind interfaces defined in your domain — the domain core should have zero imports from framework or library namespaces.' This is not a single-line patch; it is a fundamental restructuring of the entire application's dependency graph. Common mistakes confirm that ports, adapters, domain layers, and framework coupling are all intertwined. Adopting or correcting hexagonal architecture means rethinking and rewriting the system's structural shape — a full architectural rework.
Closest to 'defines the system's shape' (b9). The applies_to contexts span web, cli, and queue-worker — every context in a PHP application. The tags include architecture, design-pattern, testing, and solid. Hexagonal architecture is the most load-bearing architectural choice possible: every future feature, every infrastructure swap, every test strategy is shaped by whether this pattern is correctly applied. It is a rewrite-or-live-with-it commitment.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field explicitly states that competent developers confuse hexagonal architecture with layered architecture. The two look superficially similar — both have layers — but hexagonal inverts dependencies rather than flowing top-down, which is the opposite of what layered-architecture experience would predict. Common mistakes reinforce this: developers place ports in the wrong layer and put business logic in adapters, all because the mental model from familiar layered patterns misleads them.
Also Known As
TL;DR
Explanation
Hexagonal Architecture (Alistair Cockburn, 2005) places the application core at the centre and defines Ports — interfaces the application exposes or depends upon — and Adapters — concrete implementations that connect the core to the outside world (web controllers, database repositories, message queue consumers). The core never depends directly on frameworks, databases, or HTTP. This means the domain and use case logic can be tested with in-memory adapters, and production adapters can be swapped without touching the core. It is conceptually similar to Clean Architecture and DDD's application layer.
Diagram
flowchart LR
subgraph Core
APP[Application Services]
DOM[Domain Model]
APP --- DOM
end
HTTP[HTTP / REST] -->|Input Port| APP
CLI[CLI / Queue] -->|Input Port| APP
APP -->|Output Port| DB[(Database Adapter)]
APP -->|Output Port| EMAIL[Email Adapter]
APP -->|Output Port| CACHE[Cache Adapter]
style APP fill:#6e40c9,color:#fff
style DOM fill:#6e40c9,color:#fff
style HTTP fill:#1f6feb,color:#fff
style CLI fill:#1f6feb,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Putting framework-specific code (Eloquent models, Laravel Request) inside the domain layer.
- Defining ports (interfaces) in the infrastructure layer instead of the application layer — this inverts the dependency.
- Creating adapters that do more than translate between domain and infrastructure — business logic does not belong there.
- Over-engineering small applications with hexagonal architecture — the value appears at the scale where infrastructure changes.
Code Examples
// Application service reaching into HTTP request:
class CreateOrderUseCase {
public function execute(Request $request): Response { // Coupled to HTTP!
$data = $request->json();
// ...
return response()->json($order);
}
// Should accept a plain DTO, not an HTTP Request object
}
// Domain (centre) — no framework imports
class OrderService {
public function __construct(
private OrderRepositoryPort $orders, // Port (interface)
private PaymentPort $payment, // Port (interface)
) {}
public function place(Cart $cart): Order { ... }
}
// Adapter (outside) — implements the port
class EloquentOrderRepository implements OrderRepositoryPort {
public function save(Order $o): void { EloquentOrder::create($o->toArray()); }
}
class StripePaymentAdapter implements PaymentPort {
public function charge(Money $amount): ChargeId { ... }
}
// The domain layer never knows if it's talking to Stripe or a test fake