Symfony2學習筆記之事件分配器

Seekr發表於2013-08-20

----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物件作為唯一引數。

 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

相關文章