Ports & Adapters (PHP Worked Example)
debt(d5/e7/b7/t5)
Closest to 'specialist tool catches' (d5), deptrac and phpstan (per detection_hints.tools) can enforce dependency direction rules between domain and infrastructure layers, but require configuration.
Closest to 'cross-cutting refactor across the codebase' (e7), retrofitting ports & adapters into an existing PHP app where controllers call Eloquent/Doctrine directly requires introducing interfaces across many use cases and rewiring DI throughout.
Closest to 'strong gravitational pull' (b7), per applies_to (web/cli/queue contexts) the choice shapes how every feature is added — every use case must define ports and adapters, affecting all future development.
Closest to 'notable trap' (t5), per misconception developers think it requires a complex folder structure when it's really about dependency direction; common_mistakes show port-placement errors (interfaces in infra layer) that most devs eventually learn.
Also Known As
TL;DR
Explanation
Ports & Adapters (Alistair Cockburn, 2005) defines the application as a hexagon with ports on each face. Input ports are interfaces the application offers (CreateOrderUseCase, FindUserQuery). Output ports are interfaces the application requires (OrderRepository, EmailGateway). Adapters implement ports for specific technologies: HttpAdapter drives input ports; DoctrineAdapter implements output ports. The core never depends on adapters — the dependency always points inward. This enables testing the core without any infrastructure, and swapping adapters without touching the core.
Common Misconception
Why It Matters
Common Mistakes
- Output port interface in the infrastructure layer — interfaces belong to the application core.
- Application core importing Doctrine or Eloquent classes — the core must not know about adapters.
- One port per use case — keep ports focused; CreateOrderPort not a generic OrderPort with 20 methods.
- Not testing through ports — tests should call use cases via ports, not internal methods.
Code Examples
// Core directly depends on infrastructure — tightly coupled:
class CreateOrderUseCase {
public function __construct(
private \Doctrine\ORM\EntityManager $em, // Infrastructure in core!
private \Swift_Mailer $mailer, // Infrastructure in core!
) {}
// Cannot test without Doctrine and Swift
}
// Core depends only on own interfaces (ports):
interface OrderRepository { // Output port — in core
public function save(Order $order): void;
}
interface EmailGateway { // Output port — in core
public function sendConfirmation(Order $order): void;
}
class CreateOrderUseCase { // Core — no infrastructure imports
public function __construct(
private OrderRepository $orders, // Port — not Doctrine
private EmailGateway $email, // Port — not SwiftMailer
) {}
}
// Adapters in infrastructure layer:
class DoctrineOrderRepository implements OrderRepository { /* ... */ }
class SwiftEmailGateway implements EmailGateway { /* ... */ }
// Tests inject InMemoryOrderRepository — no DB needed