PHP事件驱动在DDD中怎么用

wen PHP项目 1

本文目录导读:

PHP事件驱动在DDD中怎么用

  1. 核心概念
  2. 在DDD中的典型流程
  3. PHP实现方案
  4. 关键设计原则
  5. 更复杂的场景:事件溯源

在领域驱动设计(DDD)中,PHP事件驱动通常通过领域事件(Domain Events)来实现,这是DDD中用于解耦核心领域逻辑与横切关注点(如日志、通知、审计)的核心模式。

下面从几个层面详细解释在PHP中如何使用事件驱动来实现DDD。

核心概念

  • 领域事件(Domain Event):领域专家关心的、发生在过去的一件事。UserRegisteredOrderPaid
  • 事件发布器(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所追求的核心目标。

抱歉,评论功能暂时关闭!