面向对象代码在确保代码的扩展性方面已经走了很长的路。通过创建有着明确职责的类,你的代码可以变得更加灵活,并且开发者可以扩展它们的子类去修改它们的行为。但如果他想与其它那些也生成他们自己子类的开发者去分享他的改变时,代码继承已经没有意义。

考虑一下真实世界的例子,你想为你的项目提供一个插件系统。该插件可能添加一个方法,在该方法执行前或后做些事情,而不干扰其它插件。这并不是一个可以通过继承和多重继承(PHP可能做到)就可以容易解决的问题,它们有着自身的缺陷。

 Symfony2事件调度用一种简单而有效的方式实现了观察者模式,它可以让这一切都变得可能,并且让你的项目真正可扩展。

就拿Symfon2的HttpKernel组件做一个简单的例子。一旦Response对象被创建,那么在该对象实际生效前让系统中的其它元素可以修改它(如:添加缓存头)是很有用的。为了实现这种可能,Symfony2内核抛出一个事件:core.response。下面是它如何工作的说明:

  • 监听器(PHP对象)告诉核心调度对象它希望监听core.response事件;
  • Symfony2内核告诉调度对象去调度core.response事件,并将该事件发给可以访问Response对象的事件对象;
  • 调度通知(如:调用方法)所有的core.response事件的监听器,允许它们对Response对象做一些修改。

事件

当一个事件被调用时,它被标识为一个唯一的名字(如:core.response),任何监听器都可以监听它。一个Event实例也被创建并发送给所有的监听器。正如你稍后所见,Event对象自身经常包含正在调用事件的数据。

命名约定

唯一的事件名可以是任意字符串,但最好能够遵循一些简单的命名约定:

  • 只使用小写字母、数字、点号(.)和下划线(_);
  • 名称空间的前缀后跟点号(如:core.);
  • 名字后面跟动词,表明采取什么动作(如:request)。

这里有一些推荐的事件名示例:

  • core.response
  • form.pre_set_data

事件名和Event对象

当调度通知监听器时,它发送一个实际的Event对象到那些监听器。基本的Event类十分简单:它包括一个停止事件传播的方法,仅此而已。

很多时候,特定事件所需的数据会和Event对象一起发送,以便监听器得到所需信息。在core.response事件示例中,创建并发送给每个监听器的Event对象其实是FilterResponseEvent类型,一个基本Event对象的子类。该类包含储如getResponse和setResponse这样的方法,以便让监听器可以得到甚至是替换Response对象。

这个故事的意思是:当为一个事件创建一个监听器时,发送给监听器的Event对象可以是一个特殊的子类,它有着从事件检索信息和响应事件的方法。

调度器

调试器是事件调度系统的中心对象。通常,创建一个调度器,它维护着监听器的注册。当一个事件通过该调度器调度时,它通知所有注册了该事件的监听器。

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

连接监听器

为了利用现有事件的优点,你需要将监听器连接到调度器,以便当事件被调用时,可以通知它。调用调度器addListener()方法会将任何有效的PHP调用和事件关联起来:

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

addListener()方法最多需要3个参数:

  • 监听器希望监听的事件名(字符串);
  • PHP调用,在其监听的事件被抛出时被通知;
  • 一个可选的优先级整数(越高越重要),在监听器被触发时用来与其它监听器比较(缺少为0)。如果两个监听器有着相同的优先级,那么它们按照被加入到调拨器的顺序执行。

`PHP调用`是一个PHP变量,被call_user_func()函数使用,并且在发送给is_callable()函数时返回true。它可以是\Closure实例,一个表示函数的字符串或表示对象方法和类方法的数组。

到目前为止,你已经明白PHP对象怎么注册成监听器。你也可以将PHPClosures注册成监听器:

  1. use Symfony\Component\EventDispatcher\Event; 
  2.  
  3. $dispatcher->addListener('foo.action', function (Event $event) { 
  4.     // will be executed when the foo.action event is dispatched 
  5. }); 

一旦监听器通过调度器注册后,它将监听直到事件被通知。在上面的例子中当foo.action事件被调用时,调度器调用AcmeListener::onFooAction方法并将Event对象作为单一的参数发送:

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

如果你使用Symfony2的MVC框架,监听器可以通过你的配置进行注册。幸运的是,监听器对象仅在需要时被实例化。

在许多情况下,一个特定事件的Event子类被发送到监听器。这样就给监听器权限去访问事件相关的信息。检查每个事件的文档或实现,以确保发送正确的Symfony\Component\EventDispatcher\Event实例。例如,core.event事件发送Symfony\Component\HttpKernel\Event\FilterResponseEvent的一个实例:

  1. use Symfony\Component\HttpKernel\Event\FilterResponseEvent 
  2.  
  3. public function onCoreResponse(FilterResponseEvent $event) 
  4.     $response = $event->getResponse(); 
  5.     $request = $event->getRequest(); 
  6.  
  7.     // ... 

创建和调拨一个事件

除了注册现有事件的监听器,你还可以创建并抛出自己的事件。这在你创建第三方库时,或者当你想保持你系统不同组件的灵活性和松耦合性是有用的。

静态事件类

假设你想创建一个新的Event,store.order,每次被调拨时在你的应用程序中都会创建命令。为了保持组织性,在你的应用程序中创建StoreEvents类,去定义和记录你的事件:

  1. namespace Acme\StoreBundle; 
  2.  
  3. final class StoreEvents 
  4.     /** 
  5.      * The store.order event is thrown each time an order is created 
  6.      * in the system. 
  7.      * 
  8.      * The event listener receives an Acme\StoreBundle\Event\FilterOrderEvent 
  9.      * instance. 
  10.      * 
  11.      * @var string 
  12.      */ 
  13.     const onStoreOrder = 'store.order'

注意,该类其实并不能做什么。StoreEvents类的目标只是成为常用事件集中信息的地方。还需要注意,特殊的FilterOrderEvent类将被发送到该事件的每个监听器。

创建一个Event对象

稍后,当你调拨这个新事件时,你将创建一个Eevent实例并将其发送给调拨器。调拨器然后将同样的实例发送到该事件的每个监听器。如果你不需要发送任何信息到你的监听器,你可以使用缺省的Symfony\Component\EventDispatcher\Event类。然而,大多数时候你需要发送该事件的信息给每个监听器。要完成这一点,你要创建一个新的类去扩展Symfony\Component\EventDispatcher\Event。

在本例中,每个监听器都需要访问一些假想的Order对象。创建一个Event类,使之成为可能:

  1. namespace Acme\StoreBundle\Event; 
  2.  
  3. use Symfony\Component\EventDispatcher\Event; 
  4. use Acme\StoreBundle\Order; 
  5.  
  6. class FilterOrderEvent extends Event 
  7.     protected $order; 
  8.  
  9.     public function __construct(Order $order) 
  10.     { 
  11.         $this->order = $order; 
  12.     } 
  13.  
  14.     public function getOrder() 
  15.     { 
  16.         return $this->order; 
  17.     } 

每个监听器现在可以通过getOrder方法去访问Order对象。

调度事件

dispatch()方法通知所有批定事件的监听器。它有两个参数:要调度的事件和要发送给每个监听器的Event实例:

  1. use Acme\StoreBundle\StoreEvents; 
  2. use Acme\StoreBundle\Order; 
  3. use Acme\StoreBundle\Event\FilterOrderEvent; 
  4.  
  5. // the order is somehow created or retrieved 
  6. $order = new Order(); 
  7. // ... 
  8.  
  9. // create the FilterOrderEvent and dispatch it 
  10. $event = new FilterOrderEvent($order); 
  11. $dispatcher->dispatch(StoreEvents::onStoreOrder, $event); 

注意指定FilterOrderEvent对象被创建并发送给dispatch方法。现在任何监听store.order事件的监听器将收到FilterOrderEvent,并可以通过getOrder方法访问Order对象:

  1. // some listener class that's been registered for onStoreOrder 
  2. use Acme\StoreBundle\Event\FilterOrderEvent; 
  3.  
  4. public function onStoreOrder(FilterOrderEvent $event) 
  5.     $order = $event->getOrder(); 
  6.     // do something to or with the order 

传递给事件调度器对象

如果你看过EventDispatcher类,你将注意到该类并不是一个单例模式(那里没有getInstance()静态方法)动作。这是有意的,因为你可能想在单个PHP请求中有着多个并发事件调度。但它也意味着你需要一种方式去将调拨器发送到需要连接或通知事件的对象。

最好的实践是注入事件调度器对象到你的对象中,也称依赖注入。

你可以使用构造器注入:

  1. class Foo 
  2.     protected $dispatcher = null; 
  3.  
  4.     public function __construct(EventDispatcher $dispatcher) 
  5.     { 
  6.         $this->dispatcher = $dispatcher; 
  7.     } 

或者Setter注入:

  1. class Foo 
  2.     protected $dispatcher = null; 
  3.  
  4.     public function setEventDispatcher(EventDispatcher $dispatcher) 
  5.     { 
  6.         $this->dispatcher = $dispatcher; 
  7.     } 

在两者之间选择确实是个人习惯的问题。往往会采用构造器注入,因为对象可以在构造时就完全被初始化。但当你有一个长长的依赖列表时,使用Setter注入则是要走的路,尤其是对于可选依赖而言。

如果你象我们在上面两个示例那样使用依赖注入,接下来你就可以使用Symfony2依赖注入组件来优雅地管理这样对象了。

使用事件订阅

监听事件最常用的方式就是通过调度器注册事件监听器。该监听器可以监听一个或更多事件,并在这些事件每次被调度时通知。

另一种监听事件的方式是通过事件订阅。事件订阅是一个PHP类,它能够准确地告诉调度器哪个事件要被订阅。它实现了EventSubscriberInterface接口,该接口要求单个名为getSubscribedEvents的静态方法。下面是订阅core.response和store.order事件的订阅示例:

  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.     static public function getSubscribedEvents() 
  8.     { 
  9.         return array( 
  10.             'core.response' => 'onCoreResponse', 
  11.             'store.order'   => 'onStoreOrder', 
  12.         ); 
  13.     } 
  14.  
  15.     public function onCoreResponse(FilterResponseEvent $event) 
  16.     { 
  17.         // ... 
  18.     } 
  19.  
  20.     public function onStoreOrder(FilterOrderEvent $event) 
  21.     { 
  22.         // ... 
  23.     } 

这与监听器类十分相似,除了该类自己可以告诉调度器它将监听哪个事件。要通过调度器注册订阅,可以使用:method:Symfony\\Component\\EventDispatcher\\EventDispatcher::addSubscriber方法:

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

调度器将自动为每个通过getSubscribedEvents返回的事件注册订阅。象监听器一样,addSubscriber有一个可选的第2参数,它可以给每个事件设置优先级。

停止事件流/传播

在有时情况下,监听器防止事件被其它监听器被调用。换句话说,监听器需要能告诉调度器去停止该事件到其它监听器的所有传播(如:不通知更多的监听器)。这可以在监听器内部通过stopPropagation()方法实现:

  1. use Acme\StoreBundle\Event\FilterOrderEvent; 
  2.  
  3. public function onStoreOrder(FilterOrderEvent $event) 
  4.     // ... 
  5.  
  6.     $event->stopPropagation(); 

任何还没有被调用的store.order监听器,现在都不能被调用了。