ZendFramework3事件驅(qū)動(dòng)架構(gòu)核心模塊zend-eventmanager

摘自VettNewlooc

前幾天看到一個(gè)知乎的網(wǎng)友提問如何在業(yè)務(wù)中避免出現(xiàn)復(fù)雜的if...else...邏輯,其中一個(gè)答友回答需要去看大型框架的實(shí)現(xiàn).由于個(gè)人認(rèn)為ZF3(ZendFramework3的簡(jiǎn)寫)的事件驅(qū)動(dòng)模塊實(shí)現(xiàn)的很優(yōu)雅,有很多值得借鑒的地方,并且恰好解決了這位網(wǎng)友的疑問.

0x00. 什么是事件驅(qū)動(dòng)

一句話解釋:先綁定,后觸發(fā)的邏輯實(shí)現(xiàn).

舉個(gè)栗子:

小明是個(gè)廚師.如果他工作在一家炒菜館.顧客進(jìn)店,點(diǎn)了叫A,B,C的三道菜,小明得到店小二后立即從自己的大腦中回憶這三種菜的做法,并排序.接著通過一系列嫻熟的操作,將三道菜按照預(yù)先的排序呈現(xiàn)給顧客.

此時(shí)來了第二個(gè)顧客,點(diǎn)了D菜和S湯以及主食N,這時(shí)候小明又被觸發(fā)D,S,N背后的動(dòng)作,最終完成任務(wù).

在上面這個(gè)例子中:

顧客:作為事件的綁定者,可以決定口味清淡與否,或者不加某種作料.

A,B,C:綁定后的事件.

店小二:負(fù)責(zé)通知,其實(shí)就是持有事件并且觸發(fā)的管理者.

小明:負(fù)責(zé)出發(fā)后的邏輯實(shí)現(xiàn).

D,S,N:下一個(gè)顧客綁定的事件.

這就是事件驅(qū)動(dòng),總結(jié)來說就是將一系列基礎(chǔ)操作邏輯封裝好,綁定給一個(gè)事件,當(dāng)這個(gè)事件被觸發(fā)時(shí),回調(diào)之前封裝好的邏輯.

1. 小明的操作就是事件的具體內(nèi)容.

2. 菜單上的菜名就是事件的名字.

3. 顧客來吃飯就是觸發(fā)一系列事件的條件.

4. 菜品就是整個(gè)事件的返回值.

如果小明工作在一家快餐店,情況將完全不同.他先將所有的菜做好,然后直接賣個(gè)顧客.

這就成了緩存機(jī)制,可能資源質(zhì)量有限,無法達(dá)到顧客最想要的程度,但是速度快.

但是飯店一般的做法就是現(xiàn)將永恒不變的資源緩存好,然后剩下的邏輯通過事件驅(qū)動(dòng)的方法,達(dá)到定制化要求.

0x01. 實(shí)現(xiàn)原理

在ZF3中,通過zend-eventmanager子模塊管理整個(gè)框架中的事件.將每個(gè)階段分拆成若干事件.

例如,在程序初始化階段(調(diào)用Zend\Mvc\Application::init($applicationConfig)),需要加載所有模塊,這時(shí)候zend-modulemanager將這階段常規(guī)觸發(fā)的事件定義為下列4個(gè):

loadModules:主要負(fù)責(zé)出發(fā)下面兩個(gè)事件.

loadModule:實(shí)例化每個(gè)模塊入口,并讀取主要配置,依賴檢查,模塊初始化,向bootstrap階段綁定事件等等.

mergeConfig:合并所有模塊的配置文件.

loadModules.post:配置所有servicemanager(Controller和Router等模塊的servicemanager).

然后在適當(dāng)?shù)臅r(shí)機(jī)由zend-eventmanager觸發(fā).

實(shí)現(xiàn)這種類似lazy service的原理是所有綁定在事件下的邏輯都是callable類型,其中包括帶有__invoke()的對(duì)象,閉包,數(shù)組構(gòu)成的callable等.

0x02. 名詞解釋

如果你不了解ZF3的事件驅(qū)動(dòng),建議先移步zend-eventmanager手冊(cè)看個(gè)大概.

你可能已經(jīng)部分或者全部的閱讀過zend-eventmanager的源碼,相信如果你在某個(gè)階段一定有疑問,在zend-eventmanager中,諸如Event,Listener,EventManager,SharedEventManager到底是什么意思,以下逐一先做解釋:

Listener:存放著主要業(yè)務(wù)邏輯的對(duì)象,在事件下綁定的多半就是Listener的方法(小明炒菜的各種操作).

Event:容器,負(fù)責(zé)存放Listener中可能用到的對(duì)象,可能是整個(gè)框架的ServiceManager,也可能是一個(gè)變量(廚房,有工具,有原材料).

EventManager:事件驅(qū)動(dòng)的管理者,負(fù)責(zé)綁定事件,存放事件,觸發(fā)事件等(店小二,負(fù)責(zé)通知小明開始按照要求做菜).

SharedEventManager:有時(shí)候需要為別的階段綁定事件.例如在ZF3加載模塊的時(shí)候要向bootstrap階段添加事件怎么辦?因?yàn)閎ootstrap不僅和加載模塊是兩個(gè)完全獨(dú)立的事件,而且在加載模塊的時(shí)候bootstrap事件還不為EventManager所知.這時(shí)候就要將事件暫時(shí)存放在SharedEventManager中,在稍后EventManager讀取bootstrap事件的時(shí)候會(huì)去查看SharedEventManager中有沒有此階段的邏輯,如果有的話一并觸發(fā)(如果顧客想給旁邊桌的顧客點(diǎn)一道菜,負(fù)責(zé)將這件事記錄下來的本子就是SharedEventManager).

0x03. zend-eventmanager

這一節(jié)將從實(shí)例化zend-eventmanager模塊開始,介紹這一模塊的使用方法.

安裝:

composer require zendframework\zend-eventmanager

然后在自己的項(xiàng)目中:

require(__DIR__ . '/vendor/autoload.php');

use Zend\EventManager\EventManager;

$events = new EventManager();

這就得到了zend-eventmanager模塊的入口實(shí)例.這時(shí)就使用其提供的接口,添加,觸發(fā)事件.

例1:綁定一個(gè)輸出hello world的匿名函數(shù)給一個(gè)叫simpleEvent的事件.

//綁定
$events->attach('simpleEvent', function () { echo 'hello world'; });

//觸發(fā)
$events->trigger('simpleEvent');

//輸出:
//hello world

例2:綁定兩個(gè)匿名函數(shù),一個(gè)輸出hello,另一個(gè)輸出XiaoMing,要求兩個(gè)按照固定順序執(zhí)行,即hello Xiaoming.

這時(shí)候就涉及到attach()方法的第三個(gè)參數(shù),優(yōu)先級(jí).

//綁定
$events->attach('simpleEvent', function () { echo 'hello '; }, 100);
//再次綁定
$events->attach('simpleEvent', function () { echo 'XiaoMing'; }, 99);

//按照優(yōu)先順序觸發(fā)
$events->trigger('simpleEvent');

//輸出:
//hello XiaoMing

例3:如果我們要向綁定的方法中傳入?yún)?shù)怎么辦?例如,我們用參數(shù)來代替hello后的輸出內(nèi)容.

//綁定并獲取默認(rèn)的Event容器
$events->attach('simpleEvent', function ($event) {
        echo 'hello ' . $event->getParams()['name'];
    }, 100);

//觸發(fā),并傳入?yún)?shù)
$events->trigger('simpleEvent', null, ['name' => 'Lily']);

//輸出
// hello Lily

這里例子中,出現(xiàn)了一個(gè)新東西Event,Event是一個(gè)容器,用于封裝所有Listener(其實(shí)就是綁定的那個(gè)匿名函數(shù))需要的數(shù)據(jù).

具體來說,Event是zend-eventmanager提供的用于封裝Listener所需上下文,變量等的一個(gè)容器,可以自己定義(稍后介紹),也可以像上文三個(gè)例子中的,讓zend-eventmanager自動(dòng)生成一個(gè)默認(rèn)的Event.

默認(rèn)的Event包括target和params以及name三大部分,target一般用于存放上下文,可以使用getTarget(),setTarget()兩個(gè)接口來操作,剩下兩部分接口類似,都是標(biāo)準(zhǔn)的get,set.params用于存放trigger中的第三個(gè)數(shù)組參數(shù),以上例為例,是指['name' => 'Lily'],而name則是當(dāng)前事件的名字,也就是上例中的simpleEvent.

zend-eventmanager在觸發(fā)綁定Listener的時(shí)候會(huì)講Event作為參數(shù)傳入.

這就是將參數(shù)傳入Listener中的方法.

trigger中的第二個(gè)參數(shù)就是target.

例4:例3的另外一種觸發(fā)方式.自己構(gòu)建Event,然后觸發(fā).

//綁定
$events->attach('simpleEvent', function ($event) {
    //調(diào)用自己實(shí)現(xiàn)的接口
    echo 'hello ' . $event->getUser();
});

use Zend\EventManager\Event;

//自己構(gòu)建Event,這是一種偷懶的方法,不推薦,讀者請(qǐng)引用
//Zend\EventManager\EventInterface認(rèn)真構(gòu)建
class MyEvent extends Event
{
    protected $user;

    public function setUser(string $user)
    {
        $this->user = $user;
    }   

    public function getUser()
    {
        return $this->user;
    }   
}   

$myEvent = new MyEvent;
//設(shè)置好事件的名字
$myEvent->setName('simpleEvent');
//自己實(shí)現(xiàn)的接口
$myEvent->setUser('Lucy');

//觸發(fā)
$events->triggerEvent($myEvent);

//輸出:
//hello Lucy

這里自己添加了接口getUser()setUser(),用于在Listener中更便捷的訪問到變量.

同時(shí)還使用了另外一種觸發(fā)方式triggerEvent(),用于直接觸發(fā)Event對(duì)象.

例5:如果需要從Listener中獲取數(shù)據(jù)怎么辦?也就是說Listener的返回值如何接收?

//綁定
$events->attach('simpleEvent', function ($event) {
    //調(diào)用自己實(shí)現(xiàn)的接口
    return 'hello ' . $event->getUser();
});

use Zend\EventManager\Event;

//自己構(gòu)建Event,這是一種偷懶的方法,不推薦,讀者請(qǐng)引用
//Zend\EventManager\EventInterface認(rèn)真構(gòu)建
class MyEvent extends Event
{
    protected $user;

    public function setUser(string $user)
    {
        $this->user = $user;
    }   

    public function getUser()
    {
        return $this->user;
    }   
}   

$myEvent = new MyEvent;
//設(shè)置好事件的名字
$myEvent->setName('simpleEvent');
//自己實(shí)現(xiàn)的接口
$myEvent->setUser('Lucy');

//觸發(fā)
$responses = $events->triggerEvent($myEvent);

//獲得返回值
echo $responses->pop();

//輸出:
//hello Lucy

由此可見zend-eventmanager將所有Listener的返回值全部封裝在$responses中,使用current(),next(),pop(),isEmpty()等接口順利取出返回值(詳見Zend\EventManager\ResponseCollection).

例6:在綁定了多個(gè)事件的時(shí)候,如何條件的停止事件繼續(xù)觸發(fā)?

到目前為止,我們可以順利的使用zend-eventmanager模塊了,但是,一個(gè)事件一旦被觸發(fā),其Listener將被盡數(shù)執(zhí)行,有沒有方法在某種條件下停止執(zhí)行呢?

zend-eventmanager還提供了兩種觸發(fā)方式triggerUntil()triggerEventUntil(),用于條件停止事件繼續(xù)執(zhí)行.

//綁定
$events->attach('simpleEvent', function () { return 'Lucy'; }, 100);
//再次綁定
$events->attach('simpleEvent', function () { return 'XiaoMing'; }, 99);
$events->attach('simpleEvent', function () { return 'Lily'; }, 98);

//當(dāng)遇到返回值XiaoMing時(shí)停止執(zhí)行
$responses = $events->triggerUntil(function ($name) {
    if($name === 'XiaoMing')
        return TRUE;
        
    return FALSE;
},'simpleEvent');

while(! $reponses->isEmpty()) {
    echo $responses->pop();
}

//輸出:
//XiaoMing
//Lucy

由上例可見,最后一個(gè)綁定的Listener沒有執(zhí)行.

triggerUntil()triggerEventUntil()兩個(gè)借口和trigger()以及triggerEvent()分類似,只不過加了第一個(gè)參數(shù),一個(gè)判斷是否停止事件繼續(xù)執(zhí)行的匿名函數(shù),傳入這個(gè)匿名函數(shù)的參數(shù)是當(dāng)前Listener的返回值.

另外,Event的stopPropagation()接口被調(diào)用也會(huì)停止當(dāng)前事件繼續(xù)執(zhí)行.這個(gè)比較簡(jiǎn)單,不做演示.

例7:假設(shè)有兩個(gè)事件event1和event2,event1先執(zhí)行,如何在event1執(zhí)行時(shí)動(dòng)態(tài)的給event2添加Listener?

use Zend\EventManager\EventManager;
use Zend\EventManager\SharedEventManager;

//實(shí)例化SharedEventManager
$shared = new SharedEventManager;

//實(shí)例化EventManager并注入SharedEventManager
$events = new EventManager($shared);

//綁定event1的Listener
$events->attach('event1', function () { echo 'listener1 from event1'; }, 100);
$events->attach('event1', function () { echo 'listener2 from event1'; }, 99);

//利用SharedEventManager向event2綁定Listener,標(biāo)識(shí)為SharedEvent
$events->getSharedManager()->attach('sharedEvent', 'event2', function () { echo 'listener3 from event1 shared'; });

//觸發(fā)event1
$events->trigger('event1');

//輸出:
//listener1 from event1
//listener2 from event1

SharedEventManager剛好可以滿足本例需求,SharedEventManager有兩個(gè)指標(biāo)來確定一個(gè)Listener何時(shí)觸發(fā),Identifiers事件名,也就是上例中的'sharedEvent'和'event2'.EventManager取得SharedEventManager中的事件時(shí),會(huì)對(duì)比EventManager當(dāng)前的Identifiers和所觸發(fā)的事件名,然后針對(duì)的觸發(fā)Listener.

以下是出發(fā)event1為event2綁定的Listener,標(biāo)識(shí)為SharedEvent.

//設(shè)置當(dāng)前EventManager的標(biāo)識(shí),以取得SharedEventManager對(duì)應(yīng)的事件.
$events->setIdentifiers(['sharedEvent', 'otherIdentifiers']);

$events->attach('event2', function () { echo 'listener4 from event2'; }, -100);

$events->trigger('event2');

//輸出:
//listener3 from event1 shared
//listener4 from event2

以上就是zend-eventmanager的主要用法.

嘗試解答那位網(wǎng)友的問題,是不是可以通過事件驅(qū)動(dòng)來解決問題?

業(yè)務(wù)中遇到較為冗長(zhǎng)if...else...時(shí),可以將其中的邏輯全部封裝為多個(gè)獨(dú)立的Listener,然后綁定到設(shè)計(jì)好的多個(gè)事件中,然后使用triggerUntil()triggerEventUntil(),傳入用于判斷的匿名函數(shù).或者根據(jù)情況動(dòng)態(tài)的綁定事件.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容