0Pricing
PHP Academy · 课时

用例与应用服务

将业务操作表达为与框架无关的用例。

用例与应用服务 是 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 反馈 — 无需本地设置。

此课程中的所有课时

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