----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