0Pricing
PHP Academy · 课时

端口与适配器详解

使用端口和可插拔适配器隔离核心。

端口与适配器详解 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。

六边形理念

端口与适配器——阿利斯泰尔·科克本提出的六边形架构——将应用绘制成一个六边形。内部是纯粹的业务逻辑。与外部世界的每次交互(超文本传输协议、数据库、队列、时钟、邮件)都要经过一个端口,而每个端口都由一个或多个适配器提供实现。这个形状没有特权性的顶部或底部:用户界面和数据库是对称的,都只是适配器。

端口就是接口

端口是由应用核心拥有的接口,用领域术语表达一种需求或 capability。它不能泄漏基础设施术语——不能包含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;
}

驱动端口与被驱动端口

端口有两种类型:

  • 驱动端口(主端口、入站端口)——外部世界调用以驱动应用的接口。通常就是用例接口。
  • 被驱动端口(次端口、出站端口)——应用调用以连接外部世界的接口:仓储、邮件发送器、时钟。

驱动适配器调用核心;核心则通过被驱动适配器向外调用。

<?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 或超文本传输协议接口客户端,而无需修改核心。

<?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
    }
}

一个驱动适配器

驱动适配器将外部触发转换为对驱动端口的调用。超文本传输协议控制器、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 ↔ 数据传输对象映射:适配器
  • SQL ↔ 实体填充:适配器
  • “VIP 获得 10% 折扣”:核心

一个端口,多个适配器

端口支持替换,甚至支持并行使用多个适配器。一个 NotificationPort 可以同时组合电子邮件、SMS 和 Slack 适配器。核心只调用一个方法;组装配置决定有多少个通道响应。

<?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;

快速检查

关于端口和适配器,哪项说法正确?

回顾

端口与适配器将核心隔离在接口之后:

  • 端口是由核心拥有的领域语言接口。
  • 驱动端口由入站适配器调用;被驱动端口由核心调用,并由出站适配器满足。
  • 适配器只转换协议和格式,绝不做业务决策。
  • 同一个端口支持多个适配器(用于测试的伪实现,以及用于扇出的组合适配器)。
  • 组合根负责连接所有组件;六边形架构不依赖框架。

常见问题解答

「端口与适配器详解」课时是免费的吗?

是的 — 「端口与适配器详解」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。

「端口与适配器详解」这节课中我会学到什么?

使用端口和可插拔适配器隔离核心。 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 PHP Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「端口与适配器详解」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 PHP Academy 课中编写并运行代码吗?

能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 从分层架构到整洁架构
  2. 端口与适配器详解
  3. 用例与应用服务
  4. 实践依赖倒置
← 返回 PHP Academy