0Pricing
PHP Academy · 课时

领域事件与领域服务

使用领域事件和领域服务记录业务事实。

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

捕获业务事实

并非所有行为都适合放在单个实体中。领域事件捕获已经发生的、有意义的业务事实(OrderPlaced),让系统的其他部分能够作出响应。领域服务承载不自然属于某个实体或值对象的领域逻辑。本课将展示这两者如何在不使聚合膨胀的情况下,让模型保持清晰地表达业务。

什么是领域事件

领域事件是领域中某件已发生事情的不可变记录,使用通用语言并以过去时命名:OrderPlaced、PaymentReceived、CustomerRelocated。它携带描述该事实的数据以及事实发生的时间。它是一种值对象:创建一次,永不改变。

<?php
final class OrderPlaced {
    public function __construct(
        public readonly string $orderId,
        public readonly int $totalCents,
        public readonly DateTimeImmutable $occurredOn = new DateTimeImmutable()
    ) {}
}
$e = new OrderPlaced('o1', 1300);
echo $e->orderId, ' @ ', $e->occurredOn->format('H:i'), PHP_EOL;

聚合记录事件

聚合根会在自身行为的一部分中记录事件,然后公开这些事件,以便应用层在事务提交之后分发它们。聚合本身不负责分发——它只负责记录。这样可以使领域不受基础设施关注点影响。

<?php
final class OrderPlaced {
    public function __construct(public readonly string $orderId) {}
}
abstract class AggregateRoot {
    private array $events = [];
    protected function record(object $e): void { $this->events[] = $e; }
    public function releaseEvents(): array {
        $out = $this->events; $this->events = []; return $out;
    }
}
final class Order extends AggregateRoot {
    public function __construct(public readonly string $id) {
        $this->record(new OrderPlaced($id));
    }
}
$o = new Order('o1');
foreach ($o->releaseEvents() as $e) { echo get_class($e), PHP_EOL; }

提交后分发

应用服务持久化聚合、提交事务,然后分发已释放的事件。提交后分发可以确保处理程序永远不会响应已回滚的事实。下面的顺序是处理电子邮件或外部调用等副作用时的安全模板。

<?php
final class PlaceOrderService {
    public function __construct(
        private object $repo,
        private object $dispatcher
    ) {}
    public function handle(object $order): void {
        // 1. persist inside a transaction
        $this->repo->save($order);   // commit happens here
        // 2. only AFTER commit, release and dispatch
        foreach ($order->releaseEvents() as $event) {
            $this->dispatcher->dispatch($event);
        }
    }
}
echo "order persisted, then events dispatched", PHP_EOL;

处理程序作出响应

每个事件都可以拥有多个处理程序,并且这些处理程序与触发事件的聚合相互解耦。PSR-14 分发器会将事件路由到已注册的监听器。添加新的响应(分析、通知)只需添加监听器,无需接触聚合。

<?php
final class OrderPlaced { public function __construct(public readonly string $orderId) {} }

final class Dispatcher {
    private array $listeners = [];
    public function on(string $event, callable $l): void { $this->listeners[$event][] = $l; }
    public function dispatch(object $event): void {
        foreach ($this->listeners[$event::class] ?? [] as $l) { $l($event); }
    }
}
$d = new Dispatcher();
$d->on(OrderPlaced::class, fn($e) => print("email for {$e->orderId}\n"));
$d->on(OrderPlaced::class, fn($e) => print("analytics for {$e->orderId}\n"));
$d->dispatch(new OrderPlaced('o1'));

跨边界事件

领域事件将必须保持一致、但不能共享同一事务的聚合解耦开来。销售上下文中的OrderPlaced可以通过事件触发另一个上下文中的库存预留。在内部,事件通常是对象;跨服务时,它们通常会成为消息代理上的集成事件。请区分内部领域事件和外部集成事件——公共契约不应当等同于您的内部模型。

什么是领域服务

领域服务承载满足以下条件的领域逻辑:

  • 涉及多个聚合或实体,因此不适合放入其中任何一个;并且
  • 它确实是一个领域概念,并以通用语言表达。

它是无状态的,并且作用于领域对象。负责从一个账户扣款并向另一个账户入账的FundsTransfer服务就是经典示例。

代码中的领域服务

该服务协调两个聚合,但不持有自己的状态。由于这段逻辑不属于任何一个账户,因此它位于以领域操作命名的服务中。

<?php
final class Account {
    public function __construct(public readonly string $id, private int $cents) {}
    public function withdraw(int $c): void {
        if ($c > $this->cents) { throw new DomainException('insufficient funds'); }
        $this->cents -= $c;
    }
    public function deposit(int $c): void { $this->cents += $c; }
    public function balance(): int { return $this->cents; }
}
final class MoneyTransferService {
    public function transfer(Account $from, Account $to, int $cents): void {
        $from->withdraw($cents);
        $to->deposit($cents);
    }
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new MoneyTransferService())->transfer($a, $b, 400);
echo $a->balance(), '/', $b->balance(), PHP_EOL; // 600/400

领域服务与应用服务

不要混淆二者:

  • 领域服务包含业务逻辑,位于领域层;它不了解事务或超文本传输协议。
  • 应用服务负责协调一个用例:从仓储加载聚合,调用领域逻辑,提交事务,并分发事件。它是一个不包含自身业务规则的轻量协调器。

避免服务陷阱

领域服务很容易被过度使用。如果逻辑可以放在实体或值对象上,就应该这样做:这是避免贫血模型的方法。只有当操作确实跨越多个聚合,或没有自然归属时,才应使用领域服务。否则,您会抽空实体中的行为,并重新构造出程序式代码。

应用服务负责协调

综合起来:精简的应用服务从仓储中加载聚合,调用领域服务处理跨聚合逻辑,持久化结果并派发事件。它只负责编排,不包含自身的业务规则。

<?php
final class Account {
    public function __construct(public readonly string $id, public int $cents) {}
}
final class MoneyTransferService { // domain service: the rule
    public function transfer(Account $from, Account $to, int $c): void {
        if ($c > $from->cents) { throw new DomainException('insufficient'); }
        $from->cents -= $c; $to->cents += $c;
    }
}
final class TransferApp { // application service: orchestration only
    public function __construct(private MoneyTransferService $domain) {}
    public function run(Account $a, Account $b, int $c): void {
        $this->domain->transfer($a, $b, $c); // then repo->save + dispatch
    }
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new TransferApp(new MoneyTransferService()))->run($a, $b, 250);
echo $a->cents, '/', $b->cents, PHP_EOL; // 750/250

快速检查

事件与服务。

回顾

您已经捕获了业务事实和无处归属的逻辑。领域事件是不可变的过去时记录,由聚合记录,再由应用层在提交后派发,从而解耦后续反应并跨越聚合边界。领域服务承载横跨多个聚合的无状态领域逻辑,与仅负责编排用例的精简应用服务不同。请谨慎使用这两者,让行为尽可能留在实体和值对象中应有的位置。

常见问题解答

「领域事件与领域服务」课时是免费的吗?

是的 — 「领域事件与领域服务」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 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. DDD 构建模块:实体与值对象
  2. 聚合、仓储与工厂
  3. 领域事件与领域服务
  4. 限界上下文与上下文映射
← 返回 PHP Academy