Symfony 的 EventDispatcher

今天在处理公司的项目的时候,发现用到了这个,因为涉及要维护,只是大致知道这么做,但是背地里的原理还是比较尴尬,所以自己翻译一下官方文档,然后写一遍 加入自己的设计模式理解。感觉还不错。 原文点这儿

symfony 的 event_dispatcher 组件的学习

面向对象编程 已经走了很长一段路 在确保代码的 可拓展性(extendsibility ) , 方法是通过 创建一个 有明确定义责任(well-dfined responsibilities )的类(class)的, 你的代码变得更加灵活(flexible ) 并且 ,一个开发者 能够拓展这个类,通过创建一个他们自己的子类,去修改父类的行为, 但是 如果他们想要分享这些行为的改变,那其他的开发者就不得不也创建自己的子类, 这样代码继承(code inheritance) 就不在是一个最好的答案了。

现在考虑一个现实生活中的例子,比如 你想要提供一个插件系统为你的项目, 一个插件应该能够添加方法,或者是 在一个方法执行前/后 做点什么事(面向切面编程? Java ee). 并且这些改动不应该会影响其他的插件。这个问题解决并不简单,通过使用单一继承(single inheritance), 即使在可以使用多继承的PHP ,这一样有自己的缺点(drawbacks)

这个时候 Symfony 的(事件分发器)EventDispatcher 组件(compontent) 就横空出世了。

关键概念 设计模式:Mediator pattern(中间人模式) ,Observer pattern(观察者模式)

先简单介绍下这俩种模式:

  • Mediator pattern(中间人模式) :

中间人模式,定义了一个对象,封装(encapsulates)了 一堆对象交互的操作集合。这个模式被认为是一种行为模式(Behavioral Pattern ),因为它能够修改程序运行中的行为。

在面向对象编程中, 一个程序 通常由很多的类, 业务逻辑和计算分布在这些类里。 不管怎样 随着类的增加到这个程序里,(尤其是 随着维护和/或重构 refactoring) 。这个类之间的通信,数据传输问题,就会变得更加复杂。这使得程序很难去阅读和维护。 此外,这也会变得很难去更新程序, 一旦任何改变 可能影响一系列的class 里的代码。

所以在中间人模式中, 类之间的通信方式被封装成了一个 中间人对象, 对象不再直接的和对方进行相互作用。 取而代之的是 通过这个中间人(mediator), 这个减少了相互作用类 之间的依赖。 进而减少耦合(coupling)。 UML: https://upload.wikimedia.org/wikipedia/commons/9/92/W3sDesign_Mediator_Design_Pattern_UML.jpg

  • Observer pattern(观察者模式):

观察者模式中,一个类被当做主题(subject) 维护一系列被称为观察者(observers)的依赖者(dependents),然后一般是通过调用依赖者的方法,自动的通知有任何状态的变更。

​ 这个主要是去实现分布式的事件处理系统, 在”事件驱动“ 的软件系统里,这个主题 通常被称为 一个事件流(stream of events)或者 事件源(stream source of events), 而观察者 被称为 事件槽 (sink of events) , ”流“(stream)的名称模拟了物理的设置,其中,观察者是物理分开的,无法控制来来自主题/事件源的 事件。

这个模式完美适配了任何通过 I/O 数据的过程。数据在CPU启动的时候不可用,但是可以随机(randomly)的到达( HTTP requests, GPIO data, user input from /keyboard/mouse…, distribute databases and blockchanis,… ) 大多现代编程语言都内置有”事件“构造, 可以用来实现观察者组件,尽管不是强制性的,但是大多数的观察者的实现都是通过使用后台线程来监听主题事件,和其他来自内核的机制支持(比如 Linux epoll) UML: https://upload.wikimedia.org/wikipedia/commons/0/01/W3sDesign_Observer_Design_Pattern_UML.jpg

以上就是用到的俩种设计模式。

现在开始 Symfony 的尝试, the HttpKernel Component 一旦一个 Response 对象被创建, 在这个对象被实际使用之前,可以运行其他系统的实例(element) 去修改这个response(eg: 添加一些缓存头部) 是非常有用的。为了实现这些, Symfony kernel 抛了一个 事件, kernel.response 以下是 怎么执行的过程:

  • 一个监听器(php 的对象)告诉了 中心分发器(central dispatcher)对象,想要监听 kernel.response 事件。

  • 然后再之后的某个时刻, 这个 Symfony kernel 告诉 分发器 对象,去分发调度(dispatcher)kernel.response.事件, 传递一个event 事件对像,能访问response 对象。

  • 事件分发器 通知(一般通过调用方法)所有的 监听了 内核事件 kernel.response 事件的监听者(listener) ,允许他们可以去对这个response 对象进行修改了。

首先用Composer 安装

composer require symfony/event-dispatcher

Usage :

​ 关于Symfony 组件的概念:

  • Events:一个事件的分发,首先要有唯一的表示名(e.g. kernel.response), event 实例 可以被创建并发送给任意多个监听了这个event的监听器(listener). 并且Event 对象通常包含有关调度事件的数据。

  • Naming Conventions: 唯一的事件命名, 可以是任意字符串,但是遵循以下约定:

    1. 使用小写字母 ,数字 , 半角句点 和半角下划线 (. _)
    2. 命名前缀,和跟着 点 (e.g. order. ,user.*)
    3. 命名结尾,使用动词 去指明这个要执行的操作。(e.g. order.placed)
  • Event Names and Event Objects: 当分发器提醒 监听器的时候后,传递一个实例化的Event 对象给 那些监听器。 这个Event 的基类,只包含一个方法去停止事件的传播(event propagation.)

  • The Dispatcher: 分发器 是事件分发系统里核心类, 通常,一个单一(single)dispatcher 被创建就维护一个注册的监听器,当一个event 被分发器分发,将会通知所有注册的监听器去处理event:

    use Symfony\Component\EventDispatcher\EventDispatcher;
    $dispatcher = new EventDispatcher();
    
  • Connection Listener: 利用现有存在的event, 开发者需要去连接监听器 到 分发器,因为需要通知event 被收到处理。 调用 分发器的 addlListener() 方法,可以 赋值(associates)任何一个有效的PHP Callable 的结构给一个event 。

    $listener = new AcmeListener();
    $dispatcher->addListener('acme.foo.action', [$listener,'onFooAction']);
      // 这个addListener() 方法, 使用三个参数
      // 1. 监听器想要监听的 event 的名字,
      // 2. 一个PHP callable 可以调用的结构体, 当指定事件被分发的时候执行。
      // 3. 一个可选优先级, 定义一个 正数或者负数(默认 0)。 值更高的 那个监听器所执行。 如果俩个监听器有同样的优先级,将会按照注册顺序执行。
    

    关于PHP callable : 就是可以call_user_func 调用的, 可以是 \Closure 实例,或者是一个实现了__invoke()方法的对象 (其实也就是一个闭包。) 也可以自己添加一个闭包。 一旦一个监听器注册到分发器上,就一直等待event 被通知。下面代码 描述了 当 acme.foo.action 事件被分发,分发器调用AcmeListener::onFooAction() 方法, 并传入一个 Event Object 参数。

    use Symfony\Contracts\EventDispatcher\Event;
      
    class AcmeListener
    {
        // ...
          // event 参数 就是当event 事件被分发 传过来的 event 对象, 通常 一个special event 子类包含着其他有用的信息, 你可以看每个被传过来的事件实例的文档或者实现。去了解下。
        public function onFooAction(Event $event)
        {
            // ... do something
        }
    }
    

Registering Event Listeners and Subscribers in the Service Container

给这些注册服务定义和打上 kernel.event_listenerkernel.envet_subcriber的tag 还不不够,不能使用。 你必须注册 一个编译通道(compiler pass) RegisterListenersPass() 在这个容器构建器里。

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\EventDispatcher\EventDispatcher;

$containerBuilder = new ContainerBuilder(new ParameterBag());
// register the compiler pass that handles the 'kernel.event_listener'
// and 'kernel.event_subscriber' service tags
$containerBuilder->addCompilerPass(new RegisterListenersPass());

$containerBuilder->register('event_dispatcher', EventDispatcher::class);

// registers an event listener
$containerBuilder->register('listener_service_id', \AcmeListener::class)
  ->addTag('kernel.event_listener', [
      'event' => 'acme.foo.action',
      'method' => 'onFooAction',
  ]);

// registers an event subscriber
$containerBuilder->register('subscriber_service_id', \AcmeSubscriber::class)
  ->addTag('kernel.event_subscriber');

RegisterListenersPass 解析类名和别名的关系, 比如 evnet_dispatcherEventDispatcher::class 的 ,它允许通过实践类的完全限定类名(FQCN, full qualified class name) 引用事件。 这个编译通道(pass)将会从一个容器的阐述中读取别名映射。 可用通过注册另外的一个编译器遍历AddEventAliasesPass 来拓展参数。

use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\EventDispatcher\EventDispatcher;

$containerBuilder = new ContainerBuilder(new ParameterBag());
$containerBuilder->addCompilerPass(new AddEventAliasesPass([
    \AcmeFooActionEvent::class => 'acme.foo.action',
]));
$containerBuilder->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING)

$containerBuilder->register('event_dispatcher', EventDispatcher::class);

// registers an event listener
$containerBuilder->register('listener_service_id', \AcmeListener::class)
    ->addTag('kernel.event_listener', [
        // will be translated to 'acme.foo.action' by RegisterListenersPass.
        'event' => \AcmeFooActionEvent::class,
        'method' => 'onFooAction',
    ]);

AddEventAliasesPass 的优先级 比RegisterListenersPass 高。

通常,这个监听器,默认传参 是event 分发器(dispatcher) 的service id 就是event_dispatcher, 事件监听器被打上了 kernel.event_listener 的tag, 事件订阅器的tag 是 kernel.evnet_subscriber . 这个别名的映射(mapping) 关系存储在 event_dispatcher.event_aliases. 你可用他们的默认值通过传入指定的值给 RegisterListenersPassAddEventAliasesPass 的构造器。

Creating and Dispatching an Event

除了 注册监听器 监听存在的events。 你也可以创建和分发你自己的是events. 这个是非常有用的,尤其当创建一个第三方库,你想要保持自己系统内部不同的组件之间的灵活和分离。

Creating an Event Class

假设你想要创建一个新的事件,order.placed 顾客每次在系统里对产品下订单。 这个事件就会被分发。 当这个事件被分发时, 你将会传入一个定制的事件实例(instance) 能够去访问创建的订单。 现在我们开始来创建一个定制的事件类,来详解这个代码过程。

namespace Acme\Store\Event;

use Acme\Store\Order;
use Symfony\Contracts\EventDispatcher\Event;

/**
 * The order.placed event is dispatched each time an order is created
 * in the system.
 */
class OrderPlacedEvent extends Event
{
    public const NAME = 'order.placed';

    protected $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    public function getOrder()
    {
        return $this->order;
    }
}

现在每个监听器 都能够访问这个订单信息, 通过调用getOrder(), 如果你不想要传入任何附加的数据给事件监听器,你也可以使用默认的Event 类,你同样可以查看文档,看 StoreEvents 类 和KernelEvents 类很相似。

Dispatch the Event

这个 dispatch() 方法 会通知所有监听了这个特定事件的监听器,这有俩个参数, 一个是Event 实例,以及事件分发调度的名称。

use Acme\Store\Event\OrderPlacedEvent;
use Acme\Store\Order;

// the order is somehow created or retrieved
$order = new Order();
// ...

// creates the OrderPlacedEvent and dispatches it
$event = new OrderPlacedEvent($order);
$dispatcher->dispatch($event, OrderPlacedEvent::NAME);

Using Event Subscribers

去监听一个事件最通用的方式,就是 在分发调度器上注册一个事件监听器,这个监听器能够监听一个到多个事件,然后每次被这些事件分发器通知触发(notified).

除了最通用的方式, 也有比较偏门的方式去监听事件: 就是 通过事件订阅器(event subscriber)去监听事件 . 一个事件订阅器是一个PHP 类, 它能够准确的告诉分发调度器(dispatcher),自己想要订阅哪个事件。 这个功能通过去实现EvenSubscriberInterface 接口,里面有一个静态方法, 叫做 getSubscribedEvents , 我们来看看下面关于订阅器订阅 kernel.responseorder.placed 事件。

namespace Acme\Store\Event;

use Acme\Store\Event\OrderPlacedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class StoreSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::RESPONSE => [
                ['onKernelResponsePre', 10],
                ['onKernelResponsePost', -10],
            ],
            OrderPlacedEvent::NAME => 'onStoreOrder',
        ];
    }

    public function onKernelResponsePre(ResponseEvent $event)
    {
        // ...
    }

    public function onKernelResponsePost(ResponseEvent $event)
    {
        // ...
    }

    public function onStoreOrder(OrderPlacedEvent $event)
    {
        // ...
    }
}

这个和监听器类 很相似,除了 订阅器 自己能够告诉分发(调度)器 自己想要监听那些事件, 为了注册一个订阅器到分发器上,使用 addSubscriber()方法

use Acme\Store\Event\StoreSubscriber;
// ...

$subscriber = new StoreSubscriber();
$dispatcher->addSubscriber($subscriber);

这个 分发器能够自动的注册这个订阅器getSubscribedEvents 返回想要监听的的所有事件。

上面的代码的含义就是 当 Response 事件触发的时候, 一次 调用onKernelResponsePreonKernelResponsePost .

Stopping Event Flow/Propagation

在一些情况下, 一个监听器阻止后续的监听器被调用 是非常有意义的,换句话就是,监听器需要有一个功能 去告知分发器停止事件扩散(Propagation)到其他的监听器。这个功能被实现, 在 监听器内部的一个 stopPropagtion() 方法:

use Acme\Store\Event\OrderPlacedEvent;

public function onStoreOrder(OrderPlacedEvent $event)
{
    // 停止。
    $event->stopPropagation();
}

现在剩下的 order.placed 不会被还没有来得及调用的监听器所调用了。

同样 ,如果事件被停止了 通过stopPropagtion 这个方法, 我们可以使用 isPropagationStopped 方法

$dispatcher->dispatch($event, 'foo.event');
if ($event->isPropagationStopped()) {
    // ...
}

EventDispatcher Aware Events and Listeners

事件分发器,总是传递被分发过的事件, 事件的名称和 事件实例的引用 给监听器, 这个会导致一些 应用,包括 在监听器内部分发其他事件,事件链,甚至于,懒加载的监听器进去分发器对象里。(因为是用的引用的问题? 导致 对一个实例的操作, 返回到分发器内部?有点疑惑 贴上原文)

The EventDispatcher always passes the dispatched event, the event’s name and a reference to itself to the listeners. This can lead to some advanced applications of the EventDispatcher including dispatching other events inside listeners, chaining events or even lazy loading listeners into the dispatcher object.

Event Name Introspection

事件分发器和被分发的事件名,作为参数传入监听器内部:

use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class Foo
{
    public function myEventListener(Event $event, $eventName, EventDispatcherInterface $dispatcher)
    {
        // ... do something with the event name
    }
}

that’s all … 怪累的, 不过这样比自己粗略看一遍的 收货要深得多, 虽然也要耗时得多, 不过对于观察者模式,和中间人模式 通过查wiki 也有很一些理解吧, 对这个Symfony 的事件分发组件,有了点初步的理解吧,接下来是看公司代码是怎么用的 了。