PHP 项目里事件驱动解耦

wen PHP项目 4

本文目录导读:

PHP 项目里事件驱动解耦

  1. 核心概念
  2. 基础实现(不依赖框架)
  3. 主流程解耦进阶:中间件与管道
  4. 生产级框架方案
  5. 事件驱动架构陷阱与最佳实践
  6. 总结:什么时候选择事件?

在 PHP 项目中实现事件驱动解耦,核心目的是让模块之间不直接依赖,通过事件(Event)和监听器(Listener)进行通信,这样可以极大提升代码的可维护性、可扩展性和可测试性。

以下是系统化的实现方案,从基础到进阶,涵盖架构设计和具体代码示例。


核心概念

  1. 事件(Event):一个描述“发生了什么”的简单数据对象(如 OrderCreated),它包含相关数据(如订单ID)。
  2. 监听器(Listener):对特定事件做出反应的业务逻辑(如发送邮件、更新库存)。
  3. 调度器(Dispatcher):负责管理事件与监听器的对应关系,并触发事件(dispatch($event))。

基础实现(不依赖框架)

在没有框架的情况下,我们可以自己写一个简单的调度器。

事件类(PSR-14 风格)

定义一个基础接口,约定事件都包含 getPayload() 或具体属性。

<?php
// app/Events/OrderCreated.php
namespace App\Events;
class OrderCreated
{
    public function __construct(
        public readonly int $orderId,
        public readonly array $orderData = []
    ) {}
}

监听器接口

<?php
// app/Listeners/ListenerInterface.php
namespace App\Listeners;
use App\Events\OrderCreated; // 或使用通用接口
interface ListenerInterface
{
    public function handle(object $event): void;
}

调度器(核心)

<?php
// app/Core/EventDispatcher.php
namespace App\Core;
class EventDispatcher
{
    private array $listeners = [];
    // 注册监听器
    public function listen(string $eventClass, callable|array $listener, int $priority = 0): void
    {
        $this->listeners[$eventClass][$priority][] = $listener;
        ksort($this->listeners[$eventClass]); // 按优先级排序
    }
    // 触发事件
    public function dispatch(object $event): void
    {
        $eventClass = get_class($event);
        if (!isset($this->listeners[$eventClass])) {
            return; // 无监听器直接返回
        }
        foreach ($this->listeners[$eventClass] as $priorityListeners) {
            foreach ($priorityListeners as $listener) {
                if (is_callable($listener)) {
                    $listener($event);
                } elseif (is_array($listener)) {
                    [$object, $method] = $listener;
                    $object->$method($event);
                }
            }
        }
    }
}

业务使用

<?php
// 启动时注册(bootstrap.php 或 ServiceProvider)
$dispatcher = new EventDispatcher();
// 方案A:使用闭包(简单场景)
$dispatcher->listen(OrderCreated::class, function (OrderCreated $event) {
    // 发送邮件逻辑
    echo "发送订单邮件: {$event->orderId}";
});
// 方案B:使用类(复杂逻辑)
$dispatcher->listen(OrderCreated::class, [new UpdateInventoryListener(), 'handle']);
// 业务代码中触发事件
class OrderService
{
    public function __construct(private EventDispatcher $dispatcher) {}
    public function createOrder(array $data): void
    {
        $orderId = 123; // 假设创建成功
        $this->dispatcher->dispatch(new OrderCreated($orderId, $data));
    }
}

主流程解耦进阶:中间件与管道

场景:订单创建时,需要依次执行“库存扣减”、“优惠券核销”、“发送通知”,如果不使用事件,我们会写出以下耦合代码:

class OrderService
{
    public function create(array $data): void
    {
        $order = $this->orders->create($data);
        // 这里直接依赖了其他服务
        $this->inventory->decrement($data['items']);
        $this->coupons->consume($data['couponId']);
        $this->notifier->send($order);
    }
}

解耦方案:使用中间件模式(管道),这种模式比事件更适合“必须按顺序执行且需要传递上下文”的场景。

<?php
// 1. 定义中间件接口
interface OrderMiddleware
{
    public function handle(array $orderData, \Closure $next): mixed;
}
// 2. 实现具体中间件
class InventoryMiddleware implements OrderMiddleware
{
    public function handle(array $orderData, \Closure $next): mixed
    {
        // 执行前置操作
        $this->inventory->decrement($orderData['items']);
        // 调用下一个中间件
        $result = $next($orderData);
        // 执行后置操作
        // ...
        return $result;
    }
}
// 3. 核心管道处理器
class Pipeline
{
    public function process($data, array $middlewares, \Closure $destination)
    {
        $pipeline = array_reduce(
            array_reverse($middlewares),
            fn($nextStack, $middleware) => fn($data) => $middleware->handle($data, $nextStack),
            $destination
        );
        return $pipeline($data);
    }
}
// 4. 使用
$pipeline = new Pipeline();
$result = $pipeline->process($orderData, [
    new InventoryMiddleware(),
    new CouponMiddleware(),
    new NotificationMiddleware(),
], function ($data) {
    // 最终目的地:这里是真正的订单创建逻辑
    echo "订单创建完毕";
});

生产级框架方案

在实际项目中,直接使用现成的成熟组件会更好,主流框架(Laravel / Symfony)已内置此功能。

Laravel(最常用的方案)

// 定义事件
class OrderCreated
{
    use Dispatchable;
    public function __construct(public Order $order) {}
}
// 定义监听器
class SendOrderEmail
{
    public function handle(OrderCreated $event): void
    {
        // 发送邮件
    }
}
// 注册映射(EventServiceProvider.php)
protected $listen = [
    OrderCreated::class => [
        SendOrderEmail::class,
        UpdateInventory::class,
    ],
];
// 触发事件
OrderCreated::dispatch($order);
// 或者
event(new OrderCreated($order));

Symfony EventDispatcher(组件化)

use Symfony\Component\EventDispatcher\EventDispatcher;
$dispatcher = new EventDispatcher();
$dispatcher->addListener(OrderCreated::class, function (OrderCreated $event) {
    // 业务逻辑
}, 10); // 优先级10
// 触发
$dispatcher->dispatch(new OrderCreated($order), OrderCreated::class);

事件驱动架构陷阱与最佳实践

“伪解耦”风险

注意:事件虽然让你不直接调用对方的类,但如果事件监听器里写了重逻辑(如数据库查询、调用外部API),本质仍然是同步阻塞。

解决:如果需要真正的异步(高并发),应结合消息队列(Redis Stream / RabbitMQ / Kafka)

// 解耦升级路径
同步事件(基础) --> 队列驱动(Queue::push) --> 消息队列(RabbitMQ)

数据共享方式

  • 不要传整个 Model 对象(如 $order),传递 orderId + 快照数据,否则监听器在异步处理时,原对象已过期。
  • 使用 DTO(数据传输对象) OrderCreatedPayload,包含 order_id, user_name, total_amount 等必要字段。

错误处理

  • 监听器内部必须try-catch,防止一个监听器失败导致整条事件链断裂。
  • 在 Laravel 中可以使用 ShouldQueue 监听器,配合 failed() 方法记录失败日志。

事件命名与粒度

  • 使用过去时表示已发生(OrderCreated)。
  • 触发事件应放在业务状态变更成功后(事务提交后),避免失败回滚后仍发邮件。

什么时候选择事件?

场景 推荐方案
A模块需要知道B模块的动作 事件(EventDispatcher)
必须保证流程顺序(不允许跳步) 管道/中间件(Pipeline)
需要异步处理且高吞吐 消息队列(RabbitMQ/Kafka)
解耦 + 异步 + 可靠投递 事件驱动 + 消息队列

代码演进路线原生闭包回调手写EventDispatcher使用Symfony Component集成Laravel Queue(队列异步化)引入消息中间件

推荐:如果项目已经使用 Laravel/Symfony,直接使用框架内置事件并开启队列驱动;如果是 PHP 原生或轻框架,引入 symfony/event-dispatcher 是最优雅、轻量的选择(功能完整且经过大规模验证)。

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