Symfony2學習筆記之事件分配器

來源:互聯網
上載者:User

標籤:blog   http   使用   os   io   strong   資料   for   


----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 string12     */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 FooListener12 {13     public function handler(GenericEvent $event)14     {15         if (isset($event[‘type‘]) && $event[‘type‘] === ‘foo‘) {16             // ... do something17         }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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.