Symfony2学习笔记之事件分配器

时间:2023-03-08 15:56:30
Symfony2学习笔记之事件分配器

Symfony2学习笔记之事件分配器

----EventDispatcher组件使用

简介:

      面向对象编程已经在确保代码的可扩展性方面走过了很长一段路。它是通过创建一些责任明确的类,让它们之间变得更加灵活,开发者可以通过继承这些类创建子类,来改变它们的行为。但是如果想将某个开发者的改变跟其它已经编写了自己子类的开发者共享,这种面向对象的继承就不再那么有用了。

举一个现实的实例,你想为你的项目提供一个插件系统。一个能够被添加到方法的插件,或者在方法执行的前后完成某些工作,而不会干扰到其它插件。这个通过单一的继承完成不是一个容易的事情,多重继承又有它的局限性。

SF2的Event Dispathcer组件通过一个简单有效的方式实现了Mediator模式,让这些需求的实现成为可能并为你的项目带来了真正的扩展性。

从HttpKernel组件的示例说起,一旦一个Response对象被创建,能够让系统中其它元素在该Response对象被真正使用之前修改它将是非常有用的。(比如添加一个缓存的头),SF2内核通过一个事件kernel.response做到了这一点.

那么它是如何工作的呢?
一个listener告诉中心dispatcher对象它想监听kernel.response事件:
在某个点上,SF2核心告诉dispatcher对象分配kernel.response事件,同时传递一个Event对象给分配的目标对象。
该Event对象可以用于访问Response对象。
Dispatcher通知所有监听kernel.response事件的监听者,允许它们对Response对象进行修改。

如果一个事件要被分配,它必须有一个能够标识自己的唯一名字(比如:kernel.response),这样任意数量的监听者都可以注册监听该名字。在分配过程中,同时会创建一个Event实例传递给所有的监听者。该Event对象本身通常会包含一些关于被分配事件的数据。

关于事件的名字可以是任意字符串,但是通常遵循如下的规则:
只使用小写字符,数字和点号以及下划线。
用命名空间名加点号作为前缀。
通常以指定发生行为的动词作为名字的结尾(比如request).

如下的定义时合法的事件名:
kernel.response
form.pre_set_data

事件的名称和具体事件对象:
当Dispatcher通知一个监听者时,它会传递一个真正的Event对象给这些监听者。Event基类非常简单,它除了包含一个用于停止事件传递的方法外,其它什么都没有。

通常特定事件的数据需要和该事件一起被传递给监听者,让监听该事件的监听者拥有足够的信息来响应事件。比如在kernel.response事件中,一个Event对象被创建并传递给了监听它的每一位监听者,该Event实例的实际类型是FilterResponseEvent,是Event基类的一个子类。该类包含了像getResponse()和setResponse()类型的方法,允许监听者获取甚至替换Response对象。

这个故事的寓意是,当创建一个某一事件的监听者时,传递给监听者的Event对象可能是其特定的子类,该类有附加的方法来从事件中获取信息并回复该事件。

事件分配器Dispatcher:
它是整个事件分配系统的中心对象。
通常情况下,只有唯一的分配器被创建,它维护者注册于它的所有监听者。
当一个事件通过Dispatcher被分配时,它会通知所有注册监听该事件的监听者。

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

将监听者注册到事件分配器:
要使用已有的事件,你需要把事件监听者关联到分配器以便它在分配事件时能够通知它们。
通过在dispatcher上面调用addListener()方法可以将任意的PHP合法调用关联到某个事件。

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

这里addListener方法接收3个参数:

监听者需要监听的事件名称字符串作为第一个参数:
一个监听事件的PHP调用
一个可选参数代表监听程序执行优先级(越大代表越重要),它觉得着监听者被触发的顺序,默认值为0。如果两个监听者优先级值相同那么按照其注册顺序执行。

注意:PHP callable是一个PHP变量,它可以被用于call_user_func()方法并当它被传入is_callable()方法时会返回一个true。 它可以是\Closure实例,一个实现了__invoke方法的对象,一个表示一个函数方法的字符串,或者表示一个对象方法或者一个类方法的数组。

到目前为止你知道了那些PHP对象可以被注册为监听者。你还可以注册PHP Closure作为事件监听者:

1 use Symfony\Component\EventDispatcher\Event;
2
3 $dispatcher->addListener('foo.action',function(Event $event){
4 //该方法将在foo.action事件被分配时执行
5 });

一旦一个监听者被注册到dispatcher,它就会一直等待该事件被通知。

在上面的实例中,当foo.action被分配时,分配器会调用AcmeListener::onFooAction方法并传入Event对象作为唯一参数。

Symfony2学习笔记之事件分配器
 1 use Symfony\Component\EventDispatcher\Event;
2
3 class AcmeListener
4 {
5 // ...
6
7 public function onFooAction(Event $event)
8 {
9 // ... 相关操作
10 }
11 }
Symfony2学习笔记之事件分配器

在很多情况下则是Event对象的一些子类被传递给指定事件的监听者。这些子类会让监听者能够通过一些附加的方法访问关于该事件的特定信息。我们通常需要查看SF2提供的文档说明或者事件的实现来决定Event事件触发时需要传入的类。

比如:kernel.event事件传入一个Symfony\Component\HttpKernel\Event\FilterResponseEvent:

Symfony2学习笔记之事件分配器
1 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
2
3 public function onKernelResponse(FilterResponseEvent $event)
4 {
5 $response = $event->getResponse();
6 $request = $event->getRequest();
7
8 // ...
9 }
Symfony2学习笔记之事件分配器

下面我们来看看创建并分配一个事件的过程:
我们除了注册监听者到已有的事件外,我们还可以创建和监听自己的事件。这对于我们创建第三方类库或者保持我们自己系统组件的灵活性和解耦分层有用。

1.首先创建静态事件类:
假设你想创建一个新事件store.order,它将会在每次订单被创建时分配。
为了让其看起来更规范,我们创建一个StoreEvents类用于定义和说明我们的事件:

Symfony2学习笔记之事件分配器
 1 namespace Acme\StoreBundle;
2
3 final class StoreEvents
4 {
5 /**
6 * store.order事件会在每次订单被创建时抛出
7 *
8 * 监听该事件的监听者会接收到一个
9 * Acme\StoreBundle\Event\FilterOrderEvent实例
10 *
11 * @var string
12 */
13 const STORE_ORDER = 'store_order';
14 }
Symfony2学习笔记之事件分配器

注意,该类没有做任何实际的工作,它的目的仅仅是定位公用事件信息集中的地方。同时我们还注意到在注释里说明了一个FilterOrderEvent对象被一同传递给监听者。

2.创建一个Event对象
接下来,当你派遣一个新事件时,你需要创建一个Event实例并传递给dispatcher。dispatcher会传递该实例到每一个监听该事件的监听者那里。如果你不需要传递任何信息给这些监听者,你可以直接使用默认的Symfony\Component\EventDispatcher\Event类。
大多时候,你需要传递关于该事件的一些信息给监听者,要完成这个目的,你需要创建一个新的扩展于Symfony\Component\EventDispatcher\Event类的新类。

在该例子中,每个监听者需要方法一些模拟的Order对象。那么需要创建一个新的Event子类来满足:

Symfony2学习笔记之事件分配器
 1 namespace Acme\StoreBundle\Event;
2
3 use Symfony\Component\EventDispatcher\Event;
4 use Acme\StoreBundle\Order;
5
6 class FilterOrderEvent extends Event
7 {
8 protected $order;
9
10 public function __construct(Order $order)
11 {
12 $this->order = $order;
13 }
14
15 public function getOrder()
16 {
17 return $this->order;
18 }
19 }
Symfony2学习笔记之事件分配器

这样每个监听者都可以通过该类的getOrder方法来访问订单对象了。

3. 分配事件
dispatch()方法通知所有的给定事件的监听者。它带有两个参数:分配事件的名字和需要传递给每个监听者的Event实例。

Symfony2学习笔记之事件分配器
 1 use Acme\StoreBundle\StoreEvents;
2 use Acme\StoreBundle\Order;
3 use Acme\StoreBundle\Event\FilterOrderEvent;
4
5 // 实例化一个需要的订单对象
6 $order = new Order();
7 // ...
8
9 // 创建 FilterOrderEvent 并分配它
10 $event = new FilterOrderEvent($order);
11 $dispatcher->dispatch(StoreEvents::STORE_ORDER, $event);
Symfony2学习笔记之事件分配器

注意,这里是一个特定的FilterOrderEvent对象被创建并传递给该事件的所有监听者,监听者们接收该对象后通过其getOrder方法访问Order对象。

Symfony2学习笔记之事件分配器
1 // 假设有一些监听者被注册到 "STORE_ORDER" 事件
2 use Acme\StoreBundle\Event\FilterOrderEvent;
3
4 public function onStoreOrder(FilterOrderEvent $event)
5 {
6 $order = $event->getOrder();
7 // 对订单进行一些处理
8 }
Symfony2学习笔记之事件分配器

4.使用事件订阅者
最常见的方式是一个事件监听者通过dispatcher注册到某个事件,该监听者可以监听一个或者多个事件并且在每次该事件被分配时获得通知。

另外一种监听事件的方式是通过一个事件订阅者来完成。
一个事件订阅者是一个PHP类,它能够告诉dispatcher到底哪些事件应该订阅。
事件订阅者实现了EventSubscriberInterface接口,它唯一需要实现的一个静态方法叫 getSubscribedEvents
下面的示例显示一个事件订阅者订阅kernel.response和store.order事件:

Symfony2学习笔记之事件分配器
 1 namespace Acme\StoreBundle\Event;
2
3 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
4 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
5
6 class StoreSubscriber implements EventSubscriberInterface
7 {
8 public static function getSubscribedEvents()
9 {
10 return array(
11 'kernel.response' => array(
12 array('onKernelResponsePre', 10),
13 array('onKernelResponseMid', 5),
14 array('onKernelResponsePost', 0),
15 ),
16 'store.order' => array('onStoreOrder', 0),
17 );
18 }
19
20 public function onKernelResponsePre(FilterResponseEvent $event)
21 {
22 // ...
23 }
24
25 public function onKernelResponseMid(FilterResponseEvent $event)
26 {
27 // ...
28 }
29
30 public function onKernelResponsePost(FilterResponseEvent $event)
31 {
32 // ...
33 }
34
35 public function onStoreOrder(FilterOrderEvent $event)
36 {
37 // ...
38 }
39 }
Symfony2学习笔记之事件分配器

它非常类似于监听者类,除了该类本身能够告诉dispatcher需要监听哪些事件除外。

要注册一个订阅者到dispatcher,需要使用dispatcher的addSubscriber()方法。

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

这里dispatcher会自动每一个订阅者的getSubscribedEvents方法返回的事件。该方法会返回一个以事件名字为索引的数组,它的值既可以是调用的方法名也可以是组合了方法名和调用优先级的数组。

上面的例子显示如何在订阅者类中注册多个监听方法到同一个事件,以及显示了如何为每个监听方法传入优先级设置。优先级数越高的方法越早被调用。
根据上面示例的定义,当kernel.response事件被分配时,其监听方法的调用顺序依次是:
onKernelResponsePre,OnKernelResponseMid和onKernelResponsePost.

5.阻止事件流/传递
有些情况下,可能有一个监听者来阻止其它监听者被调用。换句话说,监听者需要能告诉dispatcher来阻止将事件传递给后续的监听者。这个可以在一个监听者内部通过stopPropagation()方法来实现。

Symfony2学习笔记之事件分配器
1 use Acme\StoreBundle\Event\FilterOrderEvent;
2
3 public function onStoreOrder(FilterOrderEvent $event)
4 {
5 // ...
6
7 $event->stopPropagation();
8 }
Symfony2学习笔记之事件分配器

现在,任何还没有被调用的监听store.order事件的监听者将不会再被调用。

我们可以通过isPropagationStopped()方法来判断一个事件被阻止。

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

6.事件分配器知道事件和监听者
EventDispatcher总是注入一个它自己的引用到传入的event对象。这就意味着所有的监听者可以通过Dispatcher传递给自己的Event对象的getDispatcher()方法直接访问EventDispatcher对象。

这些可以导致EventDispatcher的一些高级应用,包括将监听者派遣其它事件,事件链或者更多监听者的事件延迟加载到dispatcher对象。
下面是延迟加载监听者:

Symfony2学习笔记之事件分配器
 1 use Symfony\Component\EventDispatcher\Event;
2 use Acme\StoreBundle\Event\StoreSubscriber;
3
4 class Foo
5 {
6 private $started = false;
7
8 public function myLazyListener(Event $event)
9 {
10 if(false === $this->started){
11 $subscriber = new StoreSubscriber();
12 $event->getDispatcher()->addSubscriber($subscriber);
13 }
14 $this->started = true;
15
16 //...更多代码
17 }
18 }
Symfony2学习笔记之事件分配器

从一个监听者内部派遣另外的事件:

Symfony2学习笔记之事件分配器
 1 use Symfony\Component\EventDispatcher\Event;
2
3 class Foo
4 {
5 public function myFooListener(Event $event)
6 {
7 $event->getDispatcher()->dispatch('log',$event);
8
9 //... 更多代码
10 }
11 }
Symfony2学习笔记之事件分配器

如果你的应用程序中使用多个EventDispatcher实例,你可能需要专门注入一个已知EventDispatcher实例到你的监听器。这可以通过构造函数或者setter方法注入:

Symfony2学习笔记之事件分配器
 1 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2
3 class Foo
4 {
5 protected $dispatcher = null;
6
7 public function __construct(EventDispatcherInterface $dispatcher)
8 {
9 $this->dispatcher = $dispatcher;
10 }
11 }
Symfony2学习笔记之事件分配器

setter方法注入:

Symfony2学习笔记之事件分配器
 1 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2
3 class Foo
4 {
5 protected $dispatcher = null;
6
7 public function setEventDispatcher(EventDispatcherInterface $dispatcher)
8 {
9 $this->dispatcher = $dispatcher;
10 }
11 }
Symfony2学习笔记之事件分配器

以上两种注入方法选用哪一个完全取决于个人喜好。一些人倾向于构造器注入,因为在构造时就能够完全初始化。但是当你有一个很长的依赖名单时,使用setter注入就是个可选的方式,尤其是在依赖项是可选的情况下。

7.分配器的简写使用方式:
EventDispatcher::dispatch方法总是返回一个Event对象。这样就给我们提供了很多简写的机会。比如一个不需要自定义Event对象的事件,它完全可以依靠原生的Event对象来派遣,你不需要给dispatch方法传入任何Event对象,它自己会创建一个默认的Event对象来使用。

$dispatcher->dispatch('foo.event');

更深一步,EventDispatcher总是返回被派遣的事件对象,无论是传入的还是自己内部创建的。

这样我们就可以做一些美观的简写:

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

或者:

$barEvent = new BarEvent();
$bar = $dispatcher->dispatch('foo.event',$barEvent)->getBar();

又或者:

$response = $dispatcher->dispatch('bar.event', new BarEvent())->getBar();

8.事件名称的内部自知
因为EventDispatcher在分配事件过程中早已经知道了事件的名称,事件名称又是被注入到Event对象中,所以,对于事件监听者来说完全可以通过getName()方法获取它。

这样事件名称就可以(和其它在自定义Event中包含的其它数据一样)作为监听者处理事件流程的一部分使用了。

Symfony2学习笔记之事件分配器
use Symfony\Component\EventDispatcher\Event;

class Foo
{
public function myEventListener(Event $event)
{
echo $event->getName();
}
}
Symfony2学习笔记之事件分配器

9.其它类型事件分配器:
服务容器感知的事件分配器 ContainerAwareEventDispatcher 是一个比较特殊的事件分配器实现。它耦合了服务容器,作为依赖注入组件的一部分实现。它允许把服务作为指定事件的监听者,从而让事件分配器具备了极强的性能。

服务在容器中时延迟加载的,这就意味着作为监听者使用的服务只有在一个事件被派遣后需要这些监听者时才被创建。

安装配置比较简单只需要把一个ContainerInterface注入到ContainerAwareEventDispatcher即可:

1 use Symfony\Component\DependencyInjection\ContainerBuilder;
2 use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher;
3
4 $container = new ContainerBuilder();
5 $dispatcher = new ContainerAwareEventDispatcher($container);

添加监听者:
容器知道事件分配器既可以通过直接加载特定服务,也可通过实现EventSubscriberInterface接口的实现。

下面的示例假设服务勇气已经加载了一些出现的服务:
注意服务必须在容器中标注为public的。

添加服务:
使用addListenerService()方法来连接已存在的服务定义,这里的$callback变量是一个数组:

array($serviceId, $methodName)

$dispatcher->addListenerService($eventName,array('foo','LogListener'));

添加订阅者服务:
可以通过addSubscriberService()方法添加EventSubscribers对象,这里第一个参数是订阅者服务ID,第二个参数是服务类的名称(该类必须实现了EventSubscriberInterface接口):

$dispatcher->addSubscriberService(
'kernel.store_subscriber',
'StoreSubscriber'
);

EventSubscriberInterface具体实现:

Symfony2学习笔记之事件分配器
 1 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
2 // ...
3
4 class StoreSubscriber implements EventSubscriberInterface
5 {
6 public static function getSubscribedEvents()
7 {
8 return array(
9 'kernel.response' => array(
10 array('onKernelResponsePre', 10),
11 array('onKernelResponsePost', 0),
12 ),
13 'store.order' => array('onStoreOrder', 0),
14 );
15 }
16
17 public function onKernelResponsePre(FilterResponseEvent $event)
18 {
19 // ...
20 }
21
22 public function onKernelResponsePost(FilterResponseEvent $event)
23 {
24 // ...
25 }
26
27 public function onStoreOrder(FilterOrderEvent $event)
28 {
29 // ...
30 }
31 }
Symfony2学习笔记之事件分配器

10.还有一种事件分配器叫做不变事件分配器(Immutable Event Dispatcher):
它是一个固定的事件分配器。它不能注册新的监听者或者订阅者。它使用其它事件分配器注册的监听者或者订阅者。从这个角度说它只是一个原有事件
分配器的代理。
要使用它,首先需要创建一个标准的事件分配器(EventDispatcher 或者 ContainerAwareEventDispatcher)并为其注册一些监听者或者事件订阅者。

Symfony2学习笔记之事件分配器
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();
$dispatcher->addListener('foo.action', function ($event) {
// ...
}); // ...
Symfony2学习笔记之事件分配器

然后将这个标准的事件分配器注入到一个ImmutableEventDispatcher中:

use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
// ... $immutableDispatcher = new ImmutableEventDispatcher($dispatcher);

那么从现在开始你就需要使用这个新的事件分配器了。

使用该代理事件分配器的好处是,如果你视图执行一个方法来修改该dispatcher(比如使用其addListener方法)将会收到一个 BadMethodCallException异常被抛出。

11.最后我们看一下通用的事件对象(Event Object)
在我们调用dispatcher的dispatch方法时如果不给其传入一个自定义的Event对象,那么Dispatcher会自动创建一个默认的Event对象。 这类的Event基类是由Event Dispatcher组件提供,是特意按照面向对象方式设计的API特定对象。它为复杂的应用程序提供了更加优雅可读性更强的代码。

而GenericEvent是一个方便用于那些希望在整个应用程序中都只使用一个事件对象的情况。它适合于大多数开箱即用的目标,因为它遵循了观察者模式,这种模式下事件对象封装了一个事件主题"subject",以及一些额外的可选扩展参数。

GenericEvent除了其基类Event外还拥有一个简洁的API:
__construct() 构造器可以接收事件主题和任何参数
getSubject() 获取主题
setArgument() 通过键设置一个参数
setArguments() 设置一个参数数组
getArgument() 通过键获取一个参数值
getArguments() 获取所有参数值
hasArgument() 如果某个键值存在,则返回true。

GenericEvent同时还在参数集上实现了ArrayAccess,所以可以非常方便的通过传入额外的参数。
下面是示例假设事件监听者已经被添加到dispatcher。

Symfony2学习笔记之事件分配器
 1 use Symfony\Component\EventDispatcher\GenericEvent;
2
3 $event = new GenericEvent($subject);
4 $dispatcher->dispatch('foo', $event);
5
6 class FooListener
7 {
8 public function handler(GenericEvent $event)
9 {
10 if ($event->getSubject() instanceof Foo) {
11 // ...
12 }
13 }
14 }
Symfony2学习笔记之事件分配器

通过ArrayAccess的API传入和处理事件参数:

Symfony2学习笔记之事件分配器
 1 use Symfony\Component\EventDispatcher\GenericEvent;
2
3 $event = new GenericEvent(
4 $subject,
5 array('type' => 'foo', 'counter' => 0)
6 );
7 $dispatcher->dispatch('foo', $event);
8
9 echo $event['counter'];
10
11 class FooListener
12 {
13 public function handler(GenericEvent $event)
14 {
15 if (isset($event['type']) && $event['type'] === 'foo') {
16 // ... do something
17 }
18
19 $event['counter']++;
20 }
21 }
Symfony2学习笔记之事件分配器

过滤数据:

Symfony2学习笔记之事件分配器
 1 use Symfony\Component\EventDispatcher\GenericEvent;
2
3 $event = new GenericEvent($subject, array('data' => 'foo'));
4 $dispatcher->dispatch('foo', $event);
5
6 echo $event['data'];
7
8 class FooListener
9 {
10 public function filter(GenericEvent $event)
11 {
12 strtolower($event['data']);
13 }
14 }
Symfony2学习笔记之事件分配器

我们可以在很多地方来直接使用这个GenericEvent对象。

原文链接:http://symfony.com/doc/current/components/event_dispatcher/introduction.html