0Pricing
PHP Academy · Lesson

Ports and Adapters Explained

Isolate the core with ports and pluggable adapters.

Ports and Adapters Explained is a free PHP Academy lesson on CoddyKit — lesson 2 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.

The Hexagon Idea

Ports & Adapters — Alistair Cockburn's Hexagonal Architecture — draws your application as a hexagon. Inside is pure business logic. Every interaction with the outside world (HTTP, DB, queue, clock, email) crosses a port, and each port is satisfied by one or more adapters. The shape has no privileged top or bottom: the UI and the database are symmetric, both just adapters.

Ports Are Interfaces

A port is an interface owned by the application core that expresses a need or capability in domain terms. It must not leak infrastructure vocabulary — no PDOStatement, no GuzzleResponse, no Eloquent.

<?php
// Driven (outbound) port: the core needs to persist users
interface UserRepository
{
    public function byId(UserId $id): ?User;
    public function save(User $user): void;
}

Driving vs Driven Ports

There are two flavors:

  • Driving (primary, inbound) ports — the API the outside world calls to drive the app. Usually your use-case interfaces.
  • Driven (secondary, outbound) ports — interfaces the app calls to reach the world: repositories, mailers, clocks.

Driving adapters call into the core; the core calls out through driven adapters.

<?php
// Driving (inbound) port — the public capability of the core
interface RegisterUser
{
    public function handle(string $email, string $plainPassword): UserId;
}

The Core Implements Driving Ports

The use case implements a driving port and depends on driven ports. Note: it accepts a PasswordHasher and Clock as injected ports — even time and hashing are abstracted so the core stays deterministic and testable.

<?php
final class RegisterUserService implements RegisterUser
{
    public function __construct(
        private UserRepository $users,
        private PasswordHasher $hasher,
        private Clock $clock,
    ) {}

    public function handle(string $email, string $plain): UserId {
        if ($this->users->byEmail($email)) {
            throw new EmailAlreadyTaken($email);
        }
        $user = User::register(
            $email,
            $this->hasher->hash($plain),
            $this->clock->now()
        );
        $this->users->save($user);
        return $user->id();
    }
}

A Driven Adapter

A driven adapter implements a driven port using a concrete technology. Here a PDO adapter satisfies UserRepository. Swap it for Doctrine, Redis, or an HTTP API client without touching the core.

<?php
final class PdoUserRepository implements UserRepository
{
    public function __construct(private PDO $pdo) {}

    public function byId(UserId $id): ?User {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([(string) $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ? User::fromRow($row) : null;
    }
    public function save(User $user): void {
        // INSERT ... ON CONFLICT UPDATE
    }
}

A Driving Adapter

A driving adapter translates an external trigger into a call on a driving port. An HTTP controller, a CLI command, a message consumer — all are interchangeable driving adapters for the same use case.

<?php
// CLI driving adapter
final class RegisterUserCommand
{
    public function __construct(private RegisterUser $register) {}

    public function run(array $argv): int {
        [$email, $password] = array_slice($argv, 1);
        $id = $this->register->handle($email, $password);
        fwrite(STDOUT, "Created user $id\n");
        return 0;
    }
}

In-Memory Adapters for Tests

The biggest payoff: every driven port gets a fast fake. Tests exercise the real use case against in-memory adapters, deterministic clocks, and a no-op hasher.

<?php
final class FixedClock implements Clock {
    public function __construct(private DateTimeImmutable $t) {}
    public function now(): DateTimeImmutable { return $this->t; }
}
final class PlainHasher implements PasswordHasher {
    public function hash(string $p): string { return 'h:' . $p; }
}

$service = new RegisterUserService(
    new InMemoryUsers(),
    new PlainHasher(),
    new FixedClock(new DateTimeImmutable('2026-01-01'))
);
echo 'wired OK', PHP_EOL;

Adapters Translate, Never Decide

A common mistake is letting business rules leak into adapters. The rule of thumb: an adapter only translates data formats and protocols. If you find an if about pricing, eligibility, or status inside a controller or repository, it belongs in the core.

  • JSON ↔ DTO mapping: adapter
  • SQL ↔ entity hydration: adapter
  • "VIP gets 10% off": core

One Port, Many Adapters

Ports enable substitution and even parallel adapters. A NotificationPort can have email, SMS, and Slack adapters composed together. The core fires one method; the wiring decides how many channels respond.

<?php
interface Notifier { public function send(string $to, string $msg): void; }

final class CompositeNotifier implements Notifier {
    /** @param Notifier[] $channels */
    public function __construct(private array $channels) {}
    public function send(string $to, string $msg): void {
        foreach ($this->channels as $c) $c->send($to, $msg);
    }
}

$notifier = new CompositeNotifier([new EmailNotifier(), new SmsNotifier()]);
echo 'composed', PHP_EOL;

Where the Hexagon Maps to Folders

A pragmatic PHP layout for a bounded context:

  • Domain/ — entities, value objects, domain services
  • Application/Port/In/ — driving port interfaces (use cases)
  • Application/Port/Out/ — driven port interfaces (repos, clock)
  • Application/ — use-case implementations
  • Infrastructure/Adapter/In/ — controllers, CLI, consumers
  • Infrastructure/Adapter/Out/ — PDO/Doctrine/HTTP adapters

Composition root (DI container config) wires In and Out adapters to the ports.

Testing the Whole Hexagon

Beyond unit tests, ports enable fast acceptance tests that drive the application through its primary port and assert via in-memory secondary adapters — covering a full use case without HTTP or a database. The same test suite later runs against real adapters as integration tests, giving you a layered testing strategy with no rewrite.

<?php
// Acceptance test: real use case, fake driven adapters, no I/O
$users = new InMemoryUsers();
$service = new RegisterUserService($users, new PlainHasher(),
    new FixedClock(new DateTimeImmutable('2026-01-01')));

$id = $service->handle('dev@coddykit.com', 'pw');

assert($users->byId($id) !== null);
echo 'acceptance: user persisted via in-memory adapter', PHP_EOL;

Quick Check

Which statement about ports and adapters is correct?

Recap

Ports & Adapters isolates the core behind interfaces:

  • Ports are domain-language interfaces owned by the core.
  • Driving ports are invoked by inbound adapters; driven ports are invoked by the core and satisfied by outbound adapters.
  • Adapters only translate protocols and formats — never make business decisions.
  • The same port supports many adapters (fakes for tests, composites for fan-out).
  • The composition root wires everything; the hexagon stays framework-free.

Frequently asked questions

Is the “Ports and Adapters Explained” lesson free?

Yes — the full text of “Ports and Adapters Explained” 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 “Ports and Adapters Explained”?

Isolate the core with ports and pluggable adapters. 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 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Ports and Adapters Explained” 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