本文目录导读:

在领域驱动设计(DDD)中,PHP事件驱动通常通过领域事件(Domain Events)来实现,这是DDD中用于解耦核心领域逻辑与横切关注点(如日志、通知、审计)的核心模式。
下面从几个层面详细解释在PHP中如何使用事件驱动来实现DDD。
核心概念
- 领域事件(Domain Event):领域专家关心的、发生在过去的一件事。
UserRegistered、OrderPaid。 - 事件发布器(Event Publisher):负责将事件发送给一个或多个监听器。
- 事件监听器(Event Listener):处理事件的代码,通常用于执行后续的副作用(如发送邮件、记录日志、更新读模型)。
在DDD中的典型流程
用户请求 -> 应用服务 -> 聚合根执行方法(产生事件) -> 事件发布器 -> 监听器处理
|-> 发送邮件
|-> 更新缓存
|-> 记录审计日志
PHP实现方案
使用Symfony Messenger (推荐用于生产环境)
这是PHP生态中最成熟的实现方式。
// 1. 定义领域事件(值对象)
class UserRegistered
{
public function __construct(
public readonly string $userId,
public readonly string $email,
public readonly \DateTimeImmutable $occurredOn = new \DateTimeImmutable()
) {}
}
// 2. 聚合根中记录并发布事件
class User extends AggregateRoot
{
private string $id;
private string $email;
private array $domainEvents = []; // 待发布事件
public function register(string $id, string $email): self
{
$this->id = $id;
$this->email = $email;
// 记录领域事件(重要!不要在业务方法中直接发布)
$this->domainEvents[] = new UserRegistered($id, $email);
return $this;
}
public function releaseEvents(): array
{
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}
// 3. 应用服务(负责发布事件到Messenger总线)
class RegisterUserService
{
public function __construct(
private UserRepository $repository,
private MessageBusInterface $eventBus // Symfony Messenger
) {}
public function __invoke(RegisterUserCommand $command): void
{
$user = new User();
$user->register($command->userId, $command->email);
$this->repository->save($user);
// 发布所有未发布的事件
foreach ($user->releaseEvents() as $event) {
$this->eventBus->dispatch($event);
}
}
}
// 4. 事件监听器(处理副作用)
class SendWelcomeEmailListener
{
public function __invoke(UserRegistered $event): void
{
// 处理邮件发送(完全解耦)
Mailer::send($event->email, '欢迎邮件...');
}
}
// 5. 配置Messenger(services.yaml)
framework:
messenger:
routing:
'App\Domain\Event\UserRegistered': async # 异步处理
使用自定义Event Bus(轻量级方案)
适用于小型项目或不想引入消息队列的场景。
// 事件总线接口
interface EventBus
{
public function dispatch(object $event): void;
public function subscribe(string $eventClass, callable $handler): void;
}
// 使用PHP内置的in-memory事件系统
class InMemoryEventBus implements EventBus
{
private array $handlers = [];
public function dispatch(object $event): void
{
$class = get_class($event);
foreach ($this->handlers[$class] ?? [] as $handler) {
call_user_func($handler, $event);
}
}
public function subscribe(string $eventClass, callable $handler): void
{
$this->handlers[$eventClass][] = $handler;
}
}
关键设计原则
⚠️ 事务边界问题
重要原则:同一个事务内不发送事件,正确的做法是:
传递规则:
- 在聚合层:记录事件 (
$this->recordEvent(...)) - 在应用服务层:发布事件 (
$eventBus->dispatch(...)) - 原因:避免事务未提交导致的脏数据问题,如果监听器失败,不应该回滚聚合本身的持久化。
事件应具备足够的上下文
事件应该包含所有处理它所需的信息,但不包含领域私有数据。
class OrderPaid
{
public function __construct(
public readonly string $orderId,
public readonly string $customerEmail, // 足够但不包含敏感密码
public readonly Money $amountPaid
) {}
}
更复杂的场景:事件溯源
如果采用事件溯源(Event Sourcing),事件将承担更重要的角色——作为状态存储:
class BankAccount extends AggregateRoot
{
private string $accountId;
private float $balance;
public function withdraw(float $amount): void
{
if ($amount > $this->balance) {
throw new InsufficientFundsException();
}
// 记录事件(事件溯源中,这是持久化的主要数据)
$this->recordEvent(new MoneyWithdrawn($this->accountId, $amount));
}
}
在PHP DDD中使用事件驱动的最佳实践:
| 层面 | 推荐做法 |
|---|---|
| 事件定义 | 不可变的值对象,包含occurredOn时间戳 |
| 存储事件 | 在聚合根内用数组暂存(不立即发布) |
| 发布时机 | 在应用服务层,事务提交后发布 |
| 消息中间件 | Symfony Messenger (推荐)、RabbitMQ/Bridge |
| 异常处理 | 发布失败不应影响核心业务逻辑(记录错误日志) |
| 事件粒度 | 每个事件对应一个明确的业务含义(不是技术细节) |
这种模式让你能清晰地分离业务逻辑(什么时候发生事件)和副作用处理(事件发生后做什么),正是DDD所追求的核心目标。