Symfony2學習筆記之事件分配器


----EventDispatcher組件使用html

簡介:編程

      面向對象編程已經在確保代碼的可擴展性方面走過了很長一段路。它是經過建立一些責任明確的類,讓它們之間變得更加靈活,開發者能夠經過繼承這些類建立子類,來改變它們的行爲。可是若是想將某個開發者的改變跟其它已經編寫了本身子類的開發者共享,這種面向對象的繼承就再也不那麼有用了。數組

      舉一個現實的實例,你想爲你的項目提供一個插件系統。一個可以被添加到方法的插件,或者在方法執行的先後完成某些工做,而不會干擾到其它插件。這個經過單一的繼承完成不是一個容易的事情,多重繼承又有它的侷限性。緩存

      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對象進行修改。ui


若是一個事件要被分配,它必須有一個可以標識本身的惟一名字(好比:kernel.response),這樣任意數量的監聽者均可以註冊監聽該名字。在分配過程當中,同時會建立一個Event實例傳遞給全部的監聽者。該Event對象自己一般會包含一些關於被分配事件的數據。this

關於事件的名字能夠是任意字符串,可是一般遵循以下的規則:
只使用小寫字符,數字和點號以及下劃線。
用命名空間名加點號做爲前綴。
一般以指定發生行爲的動詞做爲名字的結尾(好比request).spa

以下的定義時合法的事件名:
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對象做爲惟一參數。

複製代碼
 1 use Symfony\Component\EventDispatcher\Event;
 2 
 3 class AcmeListener
 4 {
 5      // ...
 6 
 7      public function onFooAction(Event $event)
 8     {
 9          // ... 相關操做
10      }
11 }
複製代碼

在不少狀況下則是Event對象的一些子類被傳遞給指定事件的監聽者。這些子類會讓監聽者可以經過一些附加的方法訪問關於該事件的特定信息。咱們一般須要查看SF2提供的文檔說明或者事件的實現來決定Event事件觸發時須要傳入的類。

好比:kernel.event事件傳入一個Symfony\Component\HttpKernel\Event\FilterResponseEvent:

複製代碼
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 }
複製代碼

 


下面咱們來看看建立並分配一個事件的過程:
咱們除了註冊監聽者到已有的事件外,咱們還能夠建立和監聽本身的事件。這對於咱們建立第三方類庫或者保持咱們本身系統組件的靈活性和解耦分層有用。

1.首先建立靜態事件類:
假設你想建立一個新事件store.order,它將會在每次訂單被建立時分配。
爲了讓其看起來更規範,咱們建立一個StoreEvents類用於定義和說明咱們的事件:

複製代碼
 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 }
複製代碼

注意,該類沒有作任何實際的工做,它的目的僅僅是定位公用事件信息集中的地方。同時咱們還注意到在註釋裏說明了一個FilterOrderEvent對象被一同傳遞給監聽者。

2.建立一個Event對象
接下來,當你派遣一個新事件時,你須要建立一個Event實例並傳遞給dispatcher。dispatcher會傳遞該實例到每個監聽該事件的監聽者那裏。若是你不須要傳遞任何信息給這些監聽者,你能夠直接使用默認的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 {
 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 }
複製代碼

這樣每一個監聽者均可以經過該類的getOrder方法來訪問訂單對象了。

3. 分配事件
dispatch()方法通知全部的給定事件的監聽者。它帶有兩個參數:分配事件的名字和須要傳遞給每一個監聽者的Event實例。

複製代碼
 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);
複製代碼

注意,這裏是一個特定的FilterOrderEvent對象被建立並傳遞給該事件的全部監聽者,監聽者們接收該對象後經過其getOrder方法訪問Order對象。

複製代碼
1 // 假設有一些監聽者被註冊到 "STORE_ORDER" 事件
2 use Acme\StoreBundle\Event\FilterOrderEvent;
3 
4 public function onStoreOrder(FilterOrderEvent $event)
5 {
6     $order = $event->getOrder();
7     // 對訂單進行一些處理
8 }
複製代碼

 

4.使用事件訂閱者
最多見的方式是一個事件監聽者經過dispatcher註冊到某個事件,該監聽者能夠監聽一個或者多個事件而且在每次該事件被分配時得到通知。

另一種監聽事件的方式是經過一個事件訂閱者來完成。
一個事件訂閱者是一個PHP類,它可以告訴dispatcher到底哪些事件應該訂閱。
事件訂閱者實現了EventSubscriberInterface接口,它惟一須要實現的一個靜態方法叫 getSubscribedEvents
下面的示例顯示一個事件訂閱者訂閱kernel.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 {
 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 }                        
複製代碼

它很是相似於監聽者類,除了該類自己可以告訴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()方法來實現。

複製代碼
1 use Acme\StoreBundle\Event\FilterOrderEvent;
2 
3 public function onStoreOrder(FilterOrderEvent $event)
4 {
5     // ...
6 
7     $event->stopPropagation();
8 }
複製代碼

如今,任何尚未被調用的監聽store.order事件的監聽者將不會再被調用。

咱們能夠經過isPropagationStopped()方法來判斷一個事件被阻止。

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

 


6.事件分配器知道事件和監聽者
EventDispatcher老是注入一個它本身的引用到傳入的event對象。這就意味着全部的監聽者能夠經過Dispatcher傳遞給本身的Event對象的getDispatcher()方法直接訪問EventDispatcher對象。

這些能夠致使EventDispatcher的一些高級應用,包括將監聽者派遣其它事件,事件鏈或者更多監聽者的事件延遲加載到dispatcher對象。
下面是延遲加載監聽者:

複製代碼
 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 }
複製代碼

 

從一個監聽者內部派遣另外的事件:

複製代碼
 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 }
複製代碼

 

若是你的應用程序中使用多個EventDispatcher實例,你可能須要專門注入一個已知EventDispatcher實例到你的監聽器。這能夠經過構造函數或者setter方法注入:

複製代碼
 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 }
複製代碼

setter方法注入:

複製代碼
 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 }
複製代碼

以上兩種注入方法選用哪個徹底取決於我的喜愛。一些人傾向於構造器注入,由於在構造時就可以徹底初始化。可是當你有一個很長的依賴名單時,使用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中包含的其它數據同樣)做爲監聽者處理事件流程的一部分使用了。

複製代碼
use Symfony\Component\EventDispatcher\Event;

class Foo
{
    public function myEventListener(Event $event)
    {
        echo $event->getName();
    }
}
複製代碼

 


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具體實現:

複製代碼
 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 }
複製代碼

 

10.還有一種事件分配器叫作不變事件分配器(Immutable Event Dispatcher):
它是一個固定的事件分配器。它不能註冊新的監聽者或者訂閱者。它使用其它事件分配器註冊的監聽者或者訂閱者。從這個角度說它只是一個原有事件
分配器的代理。
要使用它,首先須要建立一個標準的事件分配器(EventDispatcher 或者 ContainerAwareEventDispatcher)併爲其註冊一些監聽者或者事件訂閱者。

複製代碼
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();
$dispatcher->addListener('foo.action', function ($event) {
    // ...
});

// ...
複製代碼

而後將這個標準的事件分配器注入到一個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。

複製代碼
 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 }
複製代碼

經過ArrayAccess的API傳入和處理事件參數:

複製代碼
 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 }
複製代碼

過濾數據:

複製代碼
 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 }
複製代碼

咱們能夠在不少地方來直接使用這個GenericEvent對象。

 

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

相關文章
相關標籤/搜索