ポートとアダプターの概要
ポートと交換可能なアダプターでコアを分離します。
「ポートとアダプターの概要」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
ヘキサゴンの考え方
Alistair Cockburnが提唱したPorts & Adapters、つまりヘキサゴナルアーキテクチャでは、アプリケーションを六角形として描きます。内部には純粋なビジネスロジックがあります。外部とのあらゆるやり取り(HTTP、DB、キュー、時計、メール)はポートを通過し、各ポートは1つ以上のアダプターによって満たされます。この形には上や下といった特別な位置はありません。UIとデータベースは対称であり、どちらも単なるアダプターです。
ポートはインターフェース
ポートはアプリケーションコアが所有するインターフェースで、ドメインの用語で必要性や能力を表現します。インフラストラクチャの用語を漏らしてはいけません。PDOStatementもGuzzleResponseも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;
}駆動ポートと被駆動ポート
ポートには2種類あります。
- 駆動(プライマリ、インバウンド)ポート — 外部がアプリケーションを駆動するために呼び出すAPIです。通常はユースケースのインターフェースです。
- 被駆動(セカンダリ、アウトバウンド)ポート — アプリケーションが外部世界に到達するために呼び出すインターフェースです。リポジトリ、メーラー、時計などが該当します。
駆動アダプターはコアを呼び出し、コアは被駆動アダプターを通じて外部を呼び出します。
<?php
// Driving (inbound) port — the public capability of the core
interface RegisterUser
{
public function handle(string $email, string $plainPassword): UserId;
}コアが駆動ポートを実装する
ユースケースは駆動ポートを実装し、被駆動ポートに依存します。ここで注目すべき点は、注入されるポートとしてPasswordHasherとClockを受け取ることです。時間やハッシュ化さえも抽象化することで、コアを決定的かつテスト可能に保ちます。
<?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();
}
}被駆動アダプター
被駆動アダプターは、具体的な技術を使って被駆動ポートを実装します。ここではPDOアダプターがUserRepositoryを満たしています。コアに触れることなく、Doctrine、Redis、HTTP APIクライアントに置き換えられます。
<?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
}
}駆動アダプター
駆動アダプターは、外部からのトリガーを駆動ポートへの呼び出しに変換します。HTTPコントローラー、CLIコマンド、メッセージコンシューマーはすべて、同じユースケースに対して交換可能な駆動アダプターです。
<?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;
}
}テスト用のインメモリアダプター
最大の利点は、すべての被駆動ポートに高速なフェイクを用意できることです。テストでは、インメモリアダプター、決定的な時計、何もしないハッシャーを使って、本物のユースケースを実行します。
<?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;アダプターは翻訳するが、判断しない
よくある間違いは、ビジネスルールをアダプターに漏れ込ませることです。原則として、アダプターが行うのはデータ形式とプロトコルの変換だけです。コントローラーやリポジトリ内に、価格、利用資格、ステータスについてのifがあるなら、それはコアに置くべきです。
- JSON ↔ DTOのマッピング:アダプター
- SQL ↔ エンティティへのデータ注入:アダプター
- 「VIPは10%割引」:コア
1つのポート、多数のアダプター
ポートを使うと、置き換えや並列アダプターの構成が可能になります。NotificationPortには、メール、SMS、Slack用のアダプターを組み合わせて持たせられます。コアは1つのメソッドを呼び出し、いくつのチャネルが応答するかは配線によって決まります。
<?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;六角形アーキテクチャをフォルダーに対応付ける
境界づけられたコンテキストに適した、実用的なPHPの構成は次のとおりです。
Domain/— エンティティ、値オブジェクト、ドメインサービスApplication/Port/In/— 駆動ポートのインターフェース(ユースケース)Application/Port/Out/— 被駆動ポートのインターフェース(リポジトリ、クロック)Application/— ユースケースの実装Infrastructure/Adapter/In/— コントローラー、CLI、コンシューマーInfrastructure/Adapter/Out/— PDO/Doctrine/HTTPアダプター
コンポジションルート(DIコンテナの設定)がInとOutのアダプターをポートに接続します。
六角形アーキテクチャ全体をテストする
ユニットテストに加えて、ポートを使うと、アプリケーションを主ポート経由で動かし、インメモリの副次アダプターを通じて検証する高速な受け入れテストを実施できます。HTTPやデータベースを使わずに、ユースケース全体をカバーできます。同じテストスイートを後から実際のアダプターに対して統合テストとして実行できるため、書き直しなしで段階的なテスト戦略を構築できます。
<?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;簡単な確認
ポートとアダプターについて正しい説明はどれですか?
まとめ
Ports & Adaptersは、インターフェースを介してコアを隔離します。
- ポートは、コアが所有するドメイン用語によるインターフェースです。
- 駆動ポートは受信アダプターから呼び出され、被駆動ポートはコアから呼び出されて、送信アダプターによって満たされます。
- アダプターはプロトコルと形式を変換するだけで、ビジネス上の判断は決して行いません。
- 同じポートで多数のアダプターを利用できます(テスト用のフェイク、ファンアウト用の複合アダプターなど)。
- コンポジションルートがすべてを接続し、六角形アーキテクチャはフレームワークから独立したままになります。
よくある質問
「ポートとアダプターの概要」レッスンは無料ですか?
はい。「ポートとアダプターの概要」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「ポートとアダプターの概要」で何を学びますか?
ポートと交換可能なアダプターでコアを分離します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「ポートとアダプターの概要」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。