首先需要明確的幾個問題:
Q1、什麼是事件?
A:事件就是一個有名字的行為。當這個行為發生的時候,就稱這個事件被觸發。
Q2、監聽器又是什麼?
A:監聽器決定了事件的邏輯表達,由事件觸發。監聽器和事件往往是成對的,當然也可以是一個事件對應多個監聽器。監聽器是對事件的反應。當事件被觸發時,由監聽器做出反應。這樣一來,多個事件的觸發可以導致一個監聽器做出反應。一個事件也可以有多個監聽器做出反應。 (一句話:監聽器和事件之間的關係既可以是一對多,也可以是多對一)
Q3、事件管理器又是幹嘛的?
A:事件管理器(EventManager),從名字上就可以看出來是管理事件用的。但他怎麼管理呢?事件管理器往往會為多個事件聚合多個監聽器(這裡的事件和監聽器都是不定數【就是可以是一個也可以是多個】)。事件管理器也負責觸發事件。
一般來說我們用物件來表示事件。一個事件物件描述了事件的基本元素,包括何時以及如何觸發這個事件。
關於事件的基本元素:事件名稱、target(觸發事件的對象,一般是事件對象本身)、事件參數。之前我們講過事件相當與一個行為,在程式裡面我們常使用方法或函數來表示行為。因此事件的參數往往也是函數的參數。
另外關於Shared managers: 之前講過一個事件可以針對多個監聽器。這就是透過Shared managers實現的。 EventManager的實作包含(組合)了SharedEventManagerInterface【在建構子或setSharedManager裡面使用了程式碼注入的方式,詳情可以檢視原始碼】),而SharedEventManagerInterface描述了一個聚合監聽器的對象,這些監聽器只連接到擁有指定識別符的事件。 SharedEventManager並不會觸發事件,他只提供監聽器並連接到事件。 EventManger透過查詢SharedEventMangaer來取得具有特定識別碼的監聽器。
EventManager裡面幾個重要的行為:
1、建立事件:建立事件其實只是建立EventManagerInterface的一個實例
2、觸發事件:一般在事件行為裡面使用trigger觸發,這樣我們執行該行為的時候便可以直接觸發該事件。函數原型:trigger($eventName,$target=null,$argv=[]);$eventName一般為時間行為名(常用__FUNCTION__代替),$target則為事件物件本身可用$this代替,$argv為傳入事件的參數(一般為事件行為的參數)。
當然事件觸發方式不隻隻有trigger一種,還有triggerUntil,triggerEvent,triggerEventUntil。從名字上我們就可以看出分成兩類:trigger和triggerEvent;trigger類別只單純的觸發事件,不需要實現創建事件實例只需要一個事件名字就可以了,而trigger不僅觸發事件還順從帶觸發監聽器,需要事件實例。而有Until後綴的方法都需要一個回呼函數,每一個監聽器的結果都會傳到該回呼函數中,如果回呼函數回傳了一個true的bool值,EventManager必須讓監聽器短路。 (關於短路見下文的短迴路)
更多內容請查看官方API,或EventMangerInterface的具體註釋。
3、建立監聽器並連接到事件:
監聽器可以透過EventManager創建,也可以透過SharedEventManager建立。兩者都是使用attach方法,但參數有點不一樣。
我們先看EventManager的方式:
方法原型:attach($eventName, callable $listener, $priority = 1)
很簡單,我們只需要事件名,還有一個可調用函數,最後是優先權預設為1(zend裡面的自帶事件的優先權多為負數,所以如果你想讓自訂的監聽器優先權比較高,直接賦值一個正數就行了。
可呼叫函數也就是我們的監聽器。事件名有個特殊情況:「*」。這類似於正規匹配,將所有的事件都連接到本監聽器中。我們現在來看看SharedEventManager方式:
方法原型:attach($identifier, $eventName, callable $listener, $priority = 1);
與之前唯一不同的地方多了個identifier參數。關於identifier的原始碼註解如下:
used to pull shared signals from SharedEventManagerInterface instance;
用來從SharedEventmanager實例中拉取分享訊號。 identifier是一個數組,依照我的理解:如果一個事件(注意SharedEventmanager無法建立事件的)定義了identifier,就表示該事件是可共享的。讓後SharedEventManger實例使用attach建立監聽器的時候傳入identifier參數。 EventManager就可以使用identifier參數查詢所有的監聽器。
令人困惑的是既然有了事件名,那就可以透過事件名來查詢相關監聽器,那為何還要多此一舉的添加identifier屬性?我考慮到的是事件繼承問題:假設有一個事件類別Foo包含一個事件行為act,SubFoo繼承了Foo類別並重寫了裡面的事件行為act。兩個類別都的事件行為都具有相同的事件名act。這時候如果透過事件名稱來查詢監聽器,顯然會有衝突。這時候我們定義identifier[__CLASS__, get_class($this)],並在監聽器中指定identifier為SubFoo,顯然會符合到SubFoo類別中的事件行為act。
以上我們透過SharedEventManager可以監聽多個事件,另外我們還可以透過listener aggregates實現。透過ZendEventManagerListenerAggregateInterface,讓一個類別監聽多個事件,並連接一個或多個實例方法作為監聽器。同樣的該介面也定義了attach(EventManagerInterface $events)和detach(EventManagerInterface $events)。我們在attach的具體實作中使用EventManager的實例的方法attach監聽到多個事件。
<span style="color: #0000ff;">use</span><span style="color: #000000;"> Zend\EventManager\EventInterface; </span><span style="color: #0000ff;">use</span><span style="color: #000000;"> Zend\EventManager\EventManagerInterface; </span><span style="color: #0000ff;">use</span><span style="color: #000000;"> Zend\EventManager\ListenerAggregateInterface; </span><span style="color: #0000ff;">use</span> Zend\<span style="color: #008080;">Log</span><span style="color: #000000;">\Logger; </span><span style="color: #0000ff;">class</span> LogEvents <span style="color: #0000ff;">implements</span><span style="color: #000000;"> ListenerAggregateInterface { </span><span style="color: #0000ff;">private</span> <span style="color: #800080;">$listeners</span> =<span style="color: #000000;"> []; </span><span style="color: #0000ff;">private</span> <span style="color: #800080;">$log</span><span style="color: #000000;">; </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> __construct(Logger <span style="color: #800080;">$log</span><span style="color: #000000;">) { </span><span style="color: #800080;">$this</span>-><span style="color: #008080;">log</span> = <span style="color: #800080;">$log</span><span style="color: #000000;">; } </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> attach(EventManagerInterface <span style="color: #800080;">$events</span><span style="color: #000000;">) { </span><span style="color: #800080;">$this</span>->listeners[] = <span style="color: #800080;">$events</span>->attach('do', [<span style="color: #800080;">$this</span>, 'log'<span style="color: #000000;">]); </span><span style="color: #800080;">$this</span>->listeners[] = <span style="color: #800080;">$events</span>->attach('doSomethingElse', [<span style="color: #800080;">$this</span>, 'log'<span style="color: #000000;">]); } </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> detach(EventCollection <span style="color: #800080;">$events</span><span style="color: #000000;">) { </span><span style="color: #0000ff;">foreach</span> (<span style="color: #800080;">$this</span>->listeners <span style="color: #0000ff;">as</span> <span style="color: #800080;">$index</span> => <span style="color: #800080;">$listener</span><span style="color: #000000;">) { </span><span style="color: #800080;">$events</span>->detach(<span style="color: #800080;">$listener</span><span style="color: #000000;">); </span><span style="color: #0000ff;">unset</span>(<span style="color: #800080;">$this</span>->listeners[<span style="color: #800080;">$index</span><span style="color: #000000;">]); } } </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> <span style="color: #008080;">log</span>(EventInterface <span style="color: #800080;">$e</span><span style="color: #000000;">) { </span><span style="color: #800080;">$event</span> = <span style="color: #800080;">$e</span>-><span style="color: #000000;">getName(); </span><span style="color: #800080;">$params</span> = <span style="color: #800080;">$e</span>-><span style="color: #000000;">getParams(); </span><span style="color: #800080;">$this</span>-><span style="color: #008080;">log</span>->info(<span style="color: #008080;">sprintf</span>('%s: %s', <span style="color: #800080;">$event</span>, json_encode(<span style="color: #800080;">$params</span><span style="color: #000000;">))); } }</span>
使用Aggregate的好處:
1、允許你使用有狀態的監聽器
2、在單一的類別中組合多個相近的監聽器,並一次性連接他們
內省監聽器回傳的結果
我們有了監聽器,但如何接收他回傳的結果呢? EventManager預設實作會傳回一個ResponseCollection的實例。這個類別繼承於PHP的SplStack。基本結構是一個棧,所以允許你反序遍歷Responses。
ResponseCollection提供了有用的幾個方法:
first(): 取得第一個結果
last(): 取得最後一個結果
contains($value): 查看是否棧裡面是否包含某一個值,如果包含則回傳true,否則false。
短迴路監聽器執行:
什麼叫短迴路呢?假設你要做一件事情,直到這件事有了結果,這是一個迴路。如果你事先知道了這件事的結果(例如之前做過這件事),那你就沒比要把這件事完完全全的做完,這時候你只需要執行一個短迴路。
我們在新增EventManager的時候有一個快取機制。在一個方法中觸發一個事件,如果我們找到一個快取結果就直接回傳。如果找不到快取結果,我們就將觸發的事件快取下來以備後用。實際上和電腦硬體裡面的快取一個道理。
EventManager元件提供兩種處理的方式:1、triggerUntil();2、triggerEventUntil。這兩個方法都接受一個回呼函數作為第一個參數。如果回調函數傳回true,那執行停止。
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">function</span> someExpensiveCall(<span style="color: #800080;">$criteria1</span>, <span style="color: #800080;">$criteria2</span><span style="color: #000000;">) { </span><span style="color: #800080;">$params</span> = <span style="color: #008080;">compact</span>('criteria1', 'criteria2'<span style="color: #000000;">); </span><span style="color: #800080;">$results</span> = <span style="color: #800080;">$this</span>->getEventManager()-><span style="color: #000000;">triggerUntil( </span><span style="color: #0000ff;">function</span>(<span style="color: #800080;">$r</span><span style="color: #000000;">){ </span><span style="color: #0000ff;">return</span> (<span style="color: #800080;">$r</span><span style="color: #000000;"> instanceof SomeResultClass); }</span>, <span style="color: #ff00ff;">__FUNCTION__</span>, <span style="color: #800080;">$this</span>, <span style="color: #800080;">$params</span><span style="color: #000000;"> ); </span><span style="color: #0000ff;">if</span>(<span style="color: #800080;">$results</span>-><span style="color: #000000;">stopped()) { </span><span style="color: #0000ff;">return</span> <span style="color: #800080;">$results</span>->last()'<span style="color: #000000;"> } }</span>
從上面範例中,我們知道,如果執行停止了很有可能是因為棧裡面最後的結果滿足我們的要求。這樣一來,我們只要回傳該結果,何必還要進行多餘的計算呢?
處理在事件中停止執行,我們也可以在監聽器中停止執行。理由是我們曾經接收過某一個事件,現在我們又接收到了相同事件,理所當然的使用之前的結果就好了。在這種情況下,監聽器會呼叫stopPropagation(true),然後EventManager會直接回傳而不會繼續通知額外的監聽器。
<span style="color: #800080;">$events</span>->attach('do', <span style="color: #0000ff;">function</span>(<span style="color: #800080;">$e</span><span style="color: #000000;">) { </span><span style="color: #800080;">$e</span>-><span style="color: #000000;">stopPropagation(); </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">new</span><span style="color: #000000;"> SomeResultClass(); });</span>
当然,使用触发器范例可能会导致歧义,毕竟你并不知道最终的结果是否满足要求。
Keeping it in order.
偶尔你会关心监听器的执行顺序。我们通过监听器的优先级来控制执行顺序(上面说讲的短回路也会影响执行顺序)。每一个EventManager::attach()和SharedEventManager::attach()都会接受一个而外的参数:priority。默认情况下为1,我们可以省略该参数。如果你提供了该参数:高优先级执行的早,低优先级的可能会推迟执行。
自定义事件对象:
我们之前使用trigger()触发事件,在这同时我们也创建了事件。但trigger()的参数有限,我们只能指定事件的对象,参数,名称。实际上我们可以创建一个自定义事件,在Zendframework里面有个很重要的事件:MvcEvent。很显然MvcEvent便是一个自定义事件,该事件组合了application实例,路由器,路由匹配对象,请求和应答对象,视图模型还有结果。我们查看MvcEvent的源码会发现MvcEvent类实际上继承了Event类。同理我们的自定义事件对象也可以继承Event类或者继承MvcEvent。
<span style="color: #800080;">$event</span> = <span style="color: #0000ff;">new</span><span style="color: #000000;"> CustomEvent(); </span><span style="color: #800080;">$event</span>->setName('foo'<span style="color: #000000;">); </span><span style="color: #800080;">$event</span>->setTarget(<span style="color: #800080;">$this</span><span style="color: #000000;">); </span><span style="color: #800080;">$event</span>->setSomeKey(<span style="color: #800080;">$value</span><span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">injected with event name and target:</span> <span style="color: #800080;">$events</span>->triggerEvent(<span style="color: #800080;">$event</span><span style="color: #000000;">); </span><span style="color: #008000;">//</span><span style="color: #008000;">Use triggerEventUntil() for criteria-based short-circuiting:</span> <span style="color: #800080;">$results</span> = <span style="color: #800080;">$events</span>->triggerEventUntil(<span style="color: #800080;">$callback</span>, <span style="color: #800080;">$event</span>);
上面的代码可以看到我们使用自定义事件类创建了一个事件对象,调用相关拦截器为事件对象设置属性。我们有了事件对象还是用trigger()触发事件吗?显然不是,我们使用triggerEvent($event)方法,该方法接收一个事件对象。而triggerEventUntil有一个回调函数,该回调函数作为是否进行短回路的依据。