用例与应用服务
将业务操作表达为与框架无关的用例。
用例与应用服务 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。
用例究竟是什么
用例(又称应用服务或交互器)准确描述一个特定于应用的操作:注册用户、下单、Cancel Subscription。它协调实体和端口,以完成单一意图。关键是,它与框架无关:没有 Request,没有 Response,也没有全局辅助函数,只有可以从任何地方调用的普通 PHP。
命令与结果数据传输对象
用例接收不可变的命令数据传输对象作为输入,并返回结果数据传输对象。数据传输对象只是传递数据,不包含行为,也不包含超出结构检查范围的验证逻辑。只读属性(PHP 8.1+)使其无法被篡改。
<?php
final class RegisterUserCommand
{
public function __construct(
public readonly string $email,
public readonly string $plainPassword,
) {}
}
final class RegisterUserResult
{
public function __construct(public readonly string $userId) {}
}应用服务的主体
服务会将命令转换为领域操作。它负责应用层面的编排——检查唯一性、执行持久化、返回标识符——同时将规则委托给实体。
<?php
final class RegisterUser
{
public function __construct(
private Users $users,
private PasswordHasher $hasher,
) {}
public function __invoke(RegisterUserCommand $c): RegisterUserResult {
if ($this->users->existsByEmail($c->email)) {
throw new EmailAlreadyRegistered($c->email);
}
$user = User::register(
UserId::generate(),
new Email($c->email),
$this->hasher->hash($c->plainPassword),
);
$this->users->add($user);
return new RegisterUserResult((string) $user->id());
}
}将逻辑保留在实体中
请警惕贫血领域模型:实体只剩下获取器和设置器,而所有逻辑都位于服务中。不变条件应当属于实体。用例应当像一段简短的意图脚本,而不是堆满业务规则的代码墙。
<?php
final class User
{
private function __construct(
private UserId $id,
private Email $email,
private string $passwordHash,
private bool $active = false,
) {}
public static function register(UserId $id, Email $e, string $hash): self {
return new self($id, $e, $hash); // invariants enforced here
}
public function activate(): void {
if ($this->active) throw new AlreadyActive();
$this->active = true;
}
public function id(): UserId { return $this->id; }
}事务边界
用例是天然的事务边界:一个用例就是一个一致的工作单元。与其在服务中到处放置 beginTransaction(),不如用事务装饰器包装服务,从而让核心与持久化无关。
<?php
interface TransactionManager {
public function transactional(callable $work): mixed;
}
final class TransactionalRegisterUser
{
public function __construct(
private RegisterUser $inner,
private TransactionManager $tx,
) {}
public function __invoke(RegisterUserCommand $c): RegisterUserResult {
return $this->tx->transactional(fn() => ($this->inner)($c));
}
}验证:应当放在哪里
请将验证分为两个层次:
- 输入验证(格式、必填字段)应在用例运行前,由驱动适配器或专用验证器完成。
- 领域验证(不变条件、业务规则)应位于值对象和实体中,并抛出领域异常。
用例假定输入格式正确,并负责落实其业务含义。
<?php
final class Email
{
public function __construct(public readonly string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: $value");
}
}
}
try { new Email('nope'); } catch (Throwable $e) { echo $e->getMessage(), PHP_EOL; }
echo (new Email('a@b.com'))->value, PHP_EOL;在没有 HTTP 的情况下返回输出
以下两种模式可以在不依赖框架的情况下返回数据:
- 返回结果数据传输对象(简单、同步)。
- 输出端口 / 展示器——用例将结果推送到注入的输出边界,由适配器决定格式(JSON、HTML、CLI)。这样,连响应结构也能保持在核心之外。
<?php
interface RegisterUserOutput {
public function present(RegisterUserResult $r): void;
}
final class RegisterUserWithPresenter {
public function __construct(private Users $users, private PasswordHasher $h) {}
public function __invoke(RegisterUserCommand $c, RegisterUserOutput $out): void {
$user = User::register(UserId::generate(), new Email($c->email), $this->h->hash($c->plainPassword));
$this->users->add($user);
$out->present(new RegisterUserResult((string) $user->id()));
}
}用例产生的领域事件
用例通常会记录由实体引发的领域事件,然后在事务提交后分发这些事件。这样可以将副作用(发送欢迎邮件、更新读取模型)与核心工作流解耦。
<?php
trait RecordsEvents {
private array $events = [];
protected function record(object $e): void { $this->events[] = $e; }
public function releaseEvents(): array {
$e = $this->events; $this->events = []; return $e;
}
}
final class UserRegistered {
public function __construct(public readonly string $userId) {}
}
// Use case calls $user->releaseEvents() and hands them to a dispatcher
echo 'event recorded pattern', PHP_EOL;每个用例一个类
相比拥有十个方法的臃肿服务,更推荐使用单一操作类(一个公共方法,通常是 __invoke)。优点包括:
- 职责和命名清晰(
CancelSubscription,而不是SubscriptionService::cancel)。 - 构造函数只注入此操作所需的依赖。
- 易于使用装饰器包装(事务、日志记录、授权)。
在组合根中进行组装
用例绝不会自行创建依赖;组合根负责创建。下面是可以放入 DI 容器定义中的手动组装示例。
<?php
$pdo = new PDO('sqlite::memory:');
$users = new PdoUsers($pdo);
$hasher = new BcryptHasher();
$register = new RegisterUser($users, $hasher);
// Decorate with a transaction boundary
$register = new TransactionalRegisterUser($register, new PdoTransactionManager($pdo));
// Driving adapter calls it
$result = $register(new RegisterUserCommand('dev@coddykit.com', 's3cret!'));
echo $result->userId, PHP_EOL;通过装饰器处理横切关注点
日志记录、指标和授权属于横切关注点,应将它们排除在用例主体之外。请使用与服务共享其接口的装饰器来包装服务,这样核心就能专注于工作流,而基础设施关注点则可以围绕它进行组合。
<?php
interface RegisterUserHandler {
public function __invoke(RegisterUserCommand $c): RegisterUserResult;
}
final class LoggingRegisterUser implements RegisterUserHandler {
public function __construct(
private RegisterUserHandler $inner,
private LoggerInterface $log,
) {}
public function __invoke(RegisterUserCommand $c): RegisterUserResult {
$this->log->info('register.start', ['email' => $c->email]);
$r = ($this->inner)($c);
$this->log->info('register.ok', ['id' => $r->userId]);
return $r;
}
}快速检查
“电子邮件必须唯一且格式正确”这一规则应当放在哪里?
回顾
与框架无关的用例为您提供了清晰的应用层:
- 每个操作使用一个单一操作类,接收命令数据传输对象,返回结果数据传输对象(或将结果推送到输出端口)。
- 实体和值对象拥有不变条件;服务只负责编排,避免使用贫血模型。
- 用例是事务边界,应由装饰器包装,而不是在内部直接调用
beginTransaction。 - 将输入验证(适配器/VO)与领域验证(实体)分开。
- 领域事件可以解耦副作用;组合根负责连接依赖。
常见问题解答
「用例与应用服务」课时是免费的吗?
是的 — 「用例与应用服务」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。
「用例与应用服务」这节课中我会学到什么?
将业务操作表达为与框架无关的用例。 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PHP Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「用例与应用服务」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PHP Academy 课中编写并运行代码吗?
能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 从分层架构到整洁架构
- 端口与适配器详解
- 用例与应用服务
- 实践依赖倒置