端口与适配器详解
使用端口和可插拔适配器隔离核心。
端口与适配器详解 是 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 反馈 — 无需本地设置。
此课程中的所有课时
- 从分层架构到整洁架构
- 端口与适配器详解
- 用例与应用服务
- 实践依赖倒置