PHP 领域事件与集成事件

wen PHP项目 7

本文目录导读:

PHP 领域事件与集成事件

  1. 领域事件(Domain Events)
  2. 集成事件(Integration Events)
  3. 核心区别对比表
  4. 实战中的流程串联
  5. 注意事项与最佳实践

在 PHP 中,领域事件(Domain Events)集成事件(Integration Events) 是构建复杂业务系统(尤其是微服务架构或模块化单体架构)时用于解耦的核心概念,它们虽然名字相似,但目的、作用范围和发送方式截然不同。

以下是对两者的深度剖析及 PHP 实现示例。


领域事件(Domain Events)

核心概念

领域事件是在聚合根(Aggregate Root)内部发生重要业务变化时产生的事件,它描述的是“过去发生了什么”(订单已支付、库存已扣减),并且属于当前业务限界上下文(Bounded Context)内部

核心目的

  • 解耦聚合:在同一个进程/事务中,将“状态变更”(如修改订单状态)与“副作用”(如发送欢迎短信、更新统计表)分离开,避免在一个方法里写满 if-else。
  • 保证事务一致性(:领域事件通常在数据库事务提交后才被分发,确保你看到的永远是最终一致且持久化的数据

技术要点(PHP 实现)

领域事件通常是同步处理的(在同一次请求/事务内),或者通过简单的消息队列进行异步分发。

代码示例(无框架,纯 PHP 思路):

<?php
// 1. 定义事件对象
class OrderPaidEvent {
    public function __construct(public readonly int $orderId, public readonly float $amount) {}
}
// 2. 事件分发器(简单的 DI 容器)
class EventDispatcher {
    private array $listeners = [];
    public function addListener(string $eventClass, callable $listener): void {
        $this->listeners[$eventClass][] = $listener;
    }
    // 注意:通常在事务提交后调用,确保数据已落库
    public function dispatch(object $event): void {
        foreach ($this->listeners[get_class($event)] ?? [] as $listener) {
            $listener($event); // 同步执行
        }
    }
}
// 3. 聚合根(实体)
class Order {
    private array $domainEvents = [];
    public function markAsPaid(): void {
        $this->status = 'paid';
        // 触发事件 - 先记录在内部
        $this->domainEvents[] = new OrderPaidEvent($this->id, $this->total);
    }
    public function pullDomainEvents(): array {
        $events = $this->domainEvents;
        $this->domainEvents = []; // 清除
        return $events;
    }
}
// 4. 使用:在业务服务层
class OrderService {
    public function __construct(private EventDispatcher $dispatcher) {}
    public function payOrder(int $orderId): void {
        // ... 伪代码:$order = $repo->find($orderId);
        $order->markAsPaid();
        // $repo->save($order); // 执行 UPDATE
        // 关键点:事务提交后取出事件并调度
        foreach ($order->pullDomainEvents() as $event) {
            $this->dispatcher->dispatch($event);
        }
    }
}
// 5. 监听器
$dispatcher = new EventDispatcher();
$dispatcher->addListener(OrderPaidEvent::class, function (OrderPaidEvent $event) {
    // 发送邮件通知(实际项目中这里可能调用另一个领域服务)
    echo "Send invoice email for order: {$event->orderId}\n";
});

特点

  • 同进程内:通常不跨服务或跨数据库。
  • 事务性:依赖“事务提交后”的钩子(如 Laravel 的 afterCommit)。
  • 语言层面:强类型,类名即事件名。

集成事件(Integration Events)

核心概念

集成事件是用于在多个独立的限界上下文(服务)或外部系统之间进行通信的事件,它解决的是跨服务的数据同步业务协作问题(订单服务通知库存服务扣减库存)。

核心目的

  • 解耦微服务:不通过同步 API 调用来传递数据,而是通过事件驱动。
  • 最终一致性:由于跨系统,无法保证强一致,只能保证最终数据一致(订单支付后,库存服务和财务服务各自异步处理)。

技术要点(PHP 实现)

集成事件通常必须通过消息队列中间件(如 RabbitMQ, Kafka, Redis Stream)异步发布,它不依赖应用内的数据库事务成功。

代码示例(结合 RabbitMQ 库 php-amqplib):

<?php
// 1. 定义集成事件(通常是 Serializable 的 DTO )
class OrderPaidIntegrationEvent implements JsonSerializable {
    public function __construct(
        public readonly string $eventId,
        public readonly int $orderId,
        public readonly string $customerEmail,
        public readonly float $totalAmount,
        public readonly \DateTimeImmutable $occurredOn
    ) {}
    public function jsonSerialize(): array {
        return [
            'event_id' => $this->eventId,
            'order_id' => $this->orderId,
            'customer_email' => $this->customerEmail,
            'total_amount' => $this->totalAmount,
            'occurred_on' => $this->occurredOn->format(DATE_ATOM),
        ];
    }
}
// 2. 发布者(在领域事件监听器中被调用)
class IntegrationEventPublisher {
    public function __construct(private \PhpAmqpLib\Channel\AMQPChannel $channel) {}
    public function publish(JsonSerializable $event, string $exchangeName): void {
        $serialized = json_encode($event);
        $msg = new \PhpAmqpLib\Message\AMQPMessage($serialized, [
            'content_type' => 'application/json',
            'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
        ]);
        // 发布到交换机,路由键可以是事件名
        $this->channel->basic_publish($msg, $exchangeName, $event::class);
        echo "Published integration event: " . $event::class . "\n";
    }
}
// 3. 如何在业务中使用
class OrderService {
    public function __construct(
        private EventDispatcher $domainDispatcher,
        private IntegrationEventPublisher $integrationPublisher
    ) {
        // 监听领域事件,转换为集成事件
        $this->domainDispatcher->addListener(
            OrderPaidEvent::class,
            function (OrderPaidEvent $domainEvent) {
                // 将领域事件转换为集成事件(包含更多上下文信息)
                $integrationEvent = new OrderPaidIntegrationEvent(
                    eventId: bin2hex(random_bytes(16)),
                    orderId: $domainEvent->orderId,
                    customerEmail: 'customer@example.com', // 这里需要从 DB 查询或附加数据
                    totalAmount: $domainEvent->amount,
                    occurredOn: new \DateTimeImmutable()
                );
                $this->integrationPublisher->publish($integrationEvent, 'order_exchange');
            }
        );
    }
}
// 4. 订阅者(在另一个服务中,循环消费队列)
class OrderPaidConsumer {
    public function handle(string $messageBody): void {
        $data = json_decode($messageBody, true);
        // 这里是 库存服务 或 财务服务
        echo "Inventory service: Decrease stock for order {$data['order_id']}\n";
        // 处理失败时记录日志,并自动重试或放入死信队列
    }
}

特点

  • 跨进程/跨网络:通过中间件传输。
  • 异步:发送方不需要等待结果。
  • 容错性:需要实现重试、死信队列、幂等性处理(因为可能重复发送)。

核心区别对比表

维度 领域事件 (Domain Events) 集成事件 (Integration Events)
作用范围 单个限界上下文(服务)内部 跨限界上下文(多个服务)或多个系统
核心目的 解耦聚合根,处理内部副作用 (Side effects) 解耦服务,实现最终一致性 (Eventually Consistent)
传递方式 进程内同步调用(常配合事务提交钩子) 跨进程异步消息 (MQ / Kafka / Redis Stream)
事务关联 与本地数据库事务强相关(提交后才发送) 与数据库事务无关,只负责将事件发送到 Broker
数据完整性 依赖数据库 ACID 依赖消息队列的持久化、回执机制
事件命名 过去时态,动词+名词+ed (如 OrderCreated) 通常包含“集成”含义或更抽象的领域词
失败处理 失败通常导致整体事务回滚(同步处理) 失败通常不阻塞主流程,需异步重试/降级
监听者 同一服务的其他模块代码 其他服务的消费者 (Consumer)

实战中的流程串联

在复杂的 PHP 应用(如 Laravel + RabbitMQ)中,典型流程如下:

  1. 业务操作触发:用户点击“支付”,调用 OrderService::pay()
  2. 内部状态变更:订单聚合根修改状态为 paid,并生成领域事件 OrderPaid
  3. 持久化:保存订单到 DB(事务开启 -> 更新订单 -> 提交事务)。
  4. 领域事件分发(事务提交后)
    • A. 内部监听:向用户发送“支付成功”邮件(同步,若失败则本次请求失败,但订单已存)。
    • B. 发布集成事件:将 OrderPaid 映射为集成事件 OrderPaidIntegrationEvent,发布到 RabbitMQ 的 OrderExchange
  5. 异步消费(库存服务):
    • 监听 OrderPaidIntegrationEvent
    • 扣减库存(在自己的数据库中执行事务)。
    • 如果库存不足,发送 InventoryNotEnough 事件,通知订单服务作废订单。

注意事项与最佳实践

  1. 不要在领域事件里放敏感数据:如银行卡号,集成事件只是通知信号,具体数据应通过 API 查询获取(或只放 orderId)。
  2. 幂等性:消费集成事件时必须检查是否已处理(例如通过 eventId 查本地表),防止 MQ 重复发送导致数据错误。
  3. 版本管理:实体类(如 Order)的变更不应破坏已发布的集成事件结构,建议使用 DTO(Data Transfer Object)模式。
  4. 使用事件存储(Event Sourcing)时:领域事件可以被持久化,而集成事件则没有必要持久化(可通过 MQ 死信队列保留)。
  • 领域事件“内部通知”,用来清理代码结构,属于同一个“家”(服务)
  • 集成事件“跨墙信息”,用来让别的系统知道你的变化,属于“广播”

在 PHP 实践中,通常会先使用 symfony/event-dispatcher(或 Laravel Events)处理领域事件,再在其监听器中使用 enqueuephp-amqpliblaminas 发布集成事件到 MQ。

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