EventDispatcher组件

EventDispatcher组件提供的工具可令你的程序组件之间“通过派遣事件和监听事件”进行互相通信。

介绍 

长久以来,面向对象程序确保了代码的灵活性。通过创建组织良好而分工明确的类,你的代码变得更加灵活,而其他开发者可以用子类扩展它们并修改基类的行为。但如果有人想要与其他已经有自己子类的开发者“共享这种改变”,那么代码继承便不再适用。

思考一个现实中的例子,你要在你的项目中提供一个插件系统。A插件要能添加方法,或者在别的方法被执行之前“做一些事”,而不去干涉其他插件。这并非单体继承所能解决的简单问题,即便PHP变得能够多重继承,还是会有很多欠点。

Symfony的EventDispatcher组件以一种简单高效的方式,实现了Mediator设计模式,令所有这些事成为可能,更令你的项目具备强大的灵活性。

看一下HttpKernel组件中的简单例子,当一个Response对象被创建后,让系统中的其他元素在该响应被实际使用之前能够修改它(比如添加一些缓存头),是非常有用的。为了实现这个,Symfony kernel派出了一个kernel.response事件。以下是它的运作过程:

  • 一个listener (监听。一个PHP对象)告诉核心的dispatcher 对象,它需要监听kernel.response事件;

  • 在某一时刻,Symfony内核通知dispatcher 对象派遣kernel.response事件,并传入一个能够访问到Response对象的Event对象。

  • dispatcher要通知kernel.request事件的全部监听器(即,调用某个监听方法),允许每一个监听能够对Response对象进行修改。

安装 

你可以通过两种方式安装本组件:

用法 

事件 

当一个事件被派遣时,它有一个唯一的事件名被用于识别(比如kernel.response),该事件可以被不计数量的监听器来监听。一个Event实例将被同时创建,并被传递给所有的监听器。后面你会看到,Event对象自身往往包含着被派遣事件的数据。

命名约定 

事件名称是唯一的,可以是任何字符串,但一般会遵循以下简要命名约定:

  • 仅使用小写字母,数字,点(.)或下划线(_);

  • 在名称前缀中使用以“点”进行分隔的命名空间(如order.*,user.*);

  • 名称的末尾使用一个动词,以指出何种动作被执行(如order.placed)。

事件名称和事件对象 

当派遣器通知监听器时,它把一个真正的Event对象传入那些监听。Event类本身很简单:它包括一个用于停止event propagation的方法,没什么其他的。

参考通用表单对象以了解更多event对象之信息。

多数情况下,关于某特定事件的数据,都要被传入Event对象中,以便满足监听(listener)所需要的信息。只要是这种情况,则一个“拥有附加方法来取得或覆写这些数据”的特殊子类,在事件被派遣的时候会被传入(到监听器中)。例如,kernel.response事件使用的是FilterResponseEvent,它里面有方法能够得到、甚至覆写Response对象。

派遣器 

dispatcher(派遣器)是事件分发系统中的核心对象。总地说,一个单一的事件派遣器创建之后,它要维护全部相关监听器的“登记”。当事件通过dispatcher被派遣时,所有已注册的监听器会接到通知:

1
2
3
use Symfony\Component\EventDispatcher\EventDispatcher;
 
$dispatcher = new EventDispatcher();

连接到监听器 

要想利用已有的事件,你需要访问派遣器下的监听器,这样它(监听)才能在事件被派遣时收到通知。dispatcher中的addListener()方法被调用时,可以把任何一个有效的PHP回调(callable)关联到那个事件:

1
2
$listener = new AcmeListener();
$dispatcher->addListener('acme.action', array($listener, 'onFooAction'));

addListener()方法有三个参数:

  1. 事件名称(字符串),监听器需要监听到它;

  2. 一个PHP回调,将在特定的事件被派遣时被执行;

  3. 一个可选的整数优先级(愈高则愈重要,该监听将被愈早地触发),用于决定一个监听相对于其他监听“何时被触发”(默认值是0)。如果两个监听的优先级相同,它们将按照各自被添加到dispatcher时的顺序来执行。

一个PHP callable就是一个php变量,能够被用于call_user_func函数并且当传入is_callable()函数时返回true。它可以是一个\Closure实例,一个实现了__invoke魔术方法(就是实际上的closure)的对象,一个对应某个函数的字符串或是一个对应某个对象方法或类方法的数组。

现在,你已经看到PHP对象是如何被注册成监听的。你也可以注册PHP的Closures作为事件监听:

1
2
3
4
5
6
use Symfony\Component\EventDispatcher\Event;
 
$dispatcher->addListener('foo.action', function (Event $event) {
    // will be executed when the foo.action event is dispatched
    // (此处的代码)将在foo.action事件被派遣之后执行
});

一旦监听被注册到派遣器,它就等待着事件被通知(到自己)。上例中,当foo.action事件被派遣时,dispatcher将调用AcmeListener::onFooAction()方法,并把 Event对象作为单一的参数传给它:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\EventDispatcher\Event;
 
class AcmeListener
{
    // ...
 
    public function onFooAction(Event $event)
    {
        // ... do something 做一些事
    }
}

$event参数,就是事实派遣时所传入的那个事件类。在很多案例中,一个特殊的事件子类被附加了额外信息而传入。你可以查看相关文档,或每一个事件的代码实现,来辨清那个实例被传入(监听器的监听方法)。

在服务容器中注册事件监听

当你使用ContainerAwareEventDispatcherDependencyInjection组件时,你可以使用RegisterListenersPass来把服务标记为事件监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
 
$containerBuilder = new ContainerBuilder(new ParameterBag());
$containerBuilder->addCompilerPass(new RegisterListenersPass());
 
// register the event dispatcher service 把事件派遣器注册成服务
$containerBuilder->setDefinition('event_dispatcher', new Definition(
    'Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher',
    array(new Reference('service_container'))
));
 
// register your event listener service 把监听器注册成服务
$listener = new Definition('AcmeListener');
$listener->addTag('kernel.event_listener', array(
    'event' => 'foo.action',
    'method' => 'onFooAction',
));
$containerBuilder->setDefinition('listener_service_id', $listener);
 
// register an event subscriber 把订阅器注册成服务
$subscriber = new Definition('AcmeSubscriber');
$subscriber->addTag('kernel.event_subscriber');
$containerBuilder->setDefinition('subscriber_service_id', $subscriber);

默认情况是,listeners pass(监听传递)假设event dispatcher的service id是event_dispatcher,而且event listeners要被打上kernel.event_listener标签,或者event subscribers被打上kernel.event_subscriber标签。你可以改变这些默认的值,只需在RegisterListenerPass的构造器中传入这些自定义的值即可。

创建并派遣事件 

除了对已有的事件注册监听之外,你可以创建并派遣你自己的事件。当创建第三方类库时,以及当你想让自己系统中的两个不同的组件保持灵活性与低耦合时,这是很有用的。

创建一个Event类 

假设你要创建一个新的事件,名为order.placed。每当用户利用你的程序对产品下单时,事件被派遣。派遣时你以能传入一个自定义的事件实例,用于(让监听器)访问订单。我们从创建这个自定义事件类开始,并且加上注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace Acme\Store\Event;
 
use Symfony\Component\EventDispatcher\Event;
use Acme\Store\Order;
 
/**
 * The order.placed event is dispatched each time an order is created
 * in the system. 每当系统新建订单时,order.placed事件都会被派遣
 */
class OrderPlacedEvent extends Event
{
    const NAME = 'order.placed';
 
    protected $order;
 
    public function __construct(Order $order)
    {
        $this->order = $order;
    }
 
    public function getOrder()
    {
        return $this->order;
    }
}

现在,每个监听都可以通过getOrder()方法来访问到订单。

如果你并不需要传递附加的数据到事件监听,也可以使用默认的Event类。这时,你可以对事件做出注释,并把文件命名为通用的StoreEvents,就像KernelEvents那样。

派遣Event 

dispatch()方法可以通知给定事件的全部监听。它有两个参数:要派遣的事件名称,以及用于传给每个监听的Event实例:

1
2
3
4
5
6
7
8
9
10
11
12
use Acme\Store\Order;
use Acme\Store\Event\OrderPlacedEvent;
 
// the order is somehow created or retrieved
// 订单被以某种方式创建/或者取出
$order = new Order();
// ...
 
// create the OrderPlacedEvent and dispatch it
// 新建事件并派遣它
$event = new OrderPlacedEvent($order);
$dispatcher->dispatch(OrderPlacedEvent::NAME, $event);

注意特殊的OrderPlacedEvent对象是为了传入dispatch()方法而建立的。现在,任何监听了order.placed事件的listener都将收到OrderPlacedEvent

使用事件订阅器 

监听事件的常规方式就是去注册一个监听器给事件派遣器。这个监听器可以监听一或多个事件——每当那些事件被派遣时它都会收到通知。

另一种监听事件的方式是,通过event subscriber(订阅器)。一个事件订阅器,是一个PHP类,可以向派遣器精确通报它要订阅哪些事件。它实现了EventSubscriberInterface接口,里面有个单独的静态方法名为getSubscribedEvents()。下面这个订阅器的例子,订阅的是kernel.responseorder.placed事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace Acme\Store\Event;
 
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Acme\Store\Event\OrderPlacedEvent;
 
class StoreSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            KernelEvents::RESPONSE => array(
                array('onKernelResponsePre', 10),
                array('onKernelResponsePost', -10),
            ),
            OrderPlacedEvent::NAME => 'onStoreOrder',
        );
    }
 
    public function onKernelResponsePre(FilterResponseEvent $event)
    {
        // ...
    }
 
    public function onKernelResponsePost(FilterResponseEvent $event)
    {
        // ...
    }
 
    public function onStoreOrder(OrderPlacedEvent $event)
    {
        // ...
    }
}

这与监听类很像,除了它自己告诉派遣器“要监听哪些事件”之外。为了把这个订阅器注册给派遣器,使用addSubscriber()方法:

1
2
3
4
5
use Acme\Store\Event\StoreSubscriber;
// ...
 
$subscriber = new StoreSubscriber();
$dispatcher->addSubscriber($subscriber);

派遣器可以为getSubscribedEvents()方法所返回的每一个事件自动地注册订阅器。该方法返回的是数组,键是事件名称,值就是要调用的方法名,或是一个由“方法名和优先级”所组成的数组。上例展示了如何在订阅器中对同一事件注册若干监听方法,以及如何对监听方法传入优先级。优先级愈高,则该监听方法愈早被调用。上例中,kernel.response事件被触发时,onKernelResponsePre()onKernelResponsePost()方法将按照优先级顺序被调用。

停止Event Flow/Propagation 

在某些场合,一个监听在作动时,防止其他监听被调用是有意义的。换言之,监听器必须能够告诉派遣器,停止向后续的监听进行关于此事件的全部宣传(Propagation。也就是不再通知任何其他监听)。这可以通过在监听器中使用stopPropagation()方法来实现:

1
2
3
4
5
6
7
8
use Acme\Store\Event\OrderPlacedEvent;
 
public function onStoreOrder(OrderPlacedEvent $event)
{
    // ...
 
    $event->stopPropagation();
}

现在,order.placed事件的任何还没有被通知到监听,将 被调用。

判断一个事件是否停止了宣传是可能的,使用isPropagationStopped()方法,它返回一个布尔值:

1
2
3
4
5
// ...
$dispatcher->dispatch('foo.event', $event);
if ($event->isPropagationStopped()) {
    // ...
}

EventDispatcher Aware的事件和监听 

EventDispatcher始终向监听器传递被派遣的事件、事件名和一个对派遣器自身的引用。这可以用于EventDispatcher的一些高级技巧中,包括在监听中继续派遣其他事件,事件链(event chaining),或是将监听“懒加载”到dispatcher对象中——在下面的例子中你会看到。

派遣器快捷方式 

如果你不需要自定义的事件对象,可以直接使用原生的Event对象。你并不需要向dispatcher传入该对象,因为它将被默认创建,除非你特意传了一个事件对象:

1
$dispatcher->dispatch('order.placed');

还有,事件派遣器永远返回被派遣的事件对象,比如,传入的事件,或是被派遣器内部建立的事件。因此有以下快捷方式可用:

1
2
3
if (!$dispatcher->dispatch('foo.event')->isPropagationStopped()) {
    // ...
}

或:

1
2
$event = new OrderPlacedEvent($order);
$order = $dispatcher->dispatch('bar.event', $event)->getOrder();

以及诸如此类。

事件名称的内部检测 

EventDispatcher实例,也包括被派遣的事件名,统统被当作参数,传入监听:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
class Foo
{
    public function myEventListener(Event $event, $eventName, EventDispatcherInterface $dispatcher)
    {
        // ... do something with the event name
        // ... 可以对事件名进行一些操作
    }
}

其他派遣器 

除了对EventDispatcher进行常规应用之外,本组件还有其他一些派遣器:

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)