ZendFramework3事件驱动架构核心模块zend-eventmanager

前几天看到一个知乎的网友提问如何在业务中避免出现复杂的if...else...逻辑,其中一个答友回答需要去看大型框架的实现.由于个人认为ZF3(ZendFramework3的简写)的事件驱动模块实现的很优雅,有很多值得借鉴的地方,并且恰好解决了这位网友的疑问.以下就是ZF3事件驱动模块的详细介绍.

0x00. 什么是事件驱动

一句话解释:先绑定,后触发的逻辑实现.

举个栗子:

小明是个厨师.如果他工作在一家炒菜馆.顾客进店,点了叫A,B,C的三道菜,小明得到店小二后立即从自己的大脑中回忆这三种菜的做法,并排序.接着通过一系列娴熟的操作,将三道菜按照预先的排序呈现给顾客.

此时来了第二个顾客,点了D菜和S汤以及主食N,这时候小明又被触发D,S,N背后的动作,最终完成任务.

在上面这个例子中:

顾客:作为事件的绑定者,可以决定口味清淡与否,或者不加某种作料.

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

店小二:负责通知,其实就是持有事件并且触发的管理者.

小明:负责出发后的逻辑实现.

D,S,N:下一个顾客绑定的事件.

这就是事件驱动,总结来说就是将一系列基础操作逻辑封装好,绑定给一个事件,当这个事件被触发时,回调之前封装好的逻辑.

1. 小明的操作就是事件的具体内容.

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

3. 顾客来吃饭就是触发一系列事件的条件.

4. 菜品就是整个事件的返回值.

如果小明工作在一家快餐店,情况将完全不同.他先将所有的菜做好,然后直接卖个顾客.

这就成了缓存机制,可能资源质量有限,无法达到顾客最想要的程度,但是速度快.

但是饭店一般的做法就是现将永恒不变的资源缓存好,然后剩下的逻辑通过事件驱动的方法,达到定制化要求.

0x01. 实现原理

在ZF3中,通过zend-eventmanager子模块管理整个框架中的事件.将每个阶段分拆成若干事件.

例如,在程序初始化阶段(调用Zend\Mvc\Application::init($applicationConfig)),需要加载所有模块,这时候zend-modulemanager将这阶段常规触发的事件定义为下列4个:

loadModules:主要负责出发下面两个事件.

loadModule:实例化每个模块入口,并读取主要配置,依赖检查,模块初始化,向bootstrap阶段绑定事件等等.

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

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

然后在适当的时机由zend-eventmanager触发.

实现这种类似lazy service的原理是所有绑定在事件下的逻辑都是callable类型,其中包括带有__invoke()的对象,闭包,数组构成的callable等.

0x02. 名词解释

如果你不了解ZF3的事件驱动,建议先移步zend-eventmanager手册看个大概.

你可能已经部分或者全部的阅读过zend-eventmanager的源码,相信如果你在某个阶段一定有疑问,在zend-eventmanager中,诸如Event,Listener,EventManager,SharedEventManager到底是什么意思,以下逐一先做解释:

Listener:存放着主要业务逻辑的对象,在事件下绑定的多半就是Listener的方法(小明炒菜的各种操作).

Event:容器,负责存放Listener中可能用到的对象,可能是整个框架的ServiceManager,也可能是一个变量(厨房,有工具,有原材料).

EventManager:事件驱动的管理者,负责绑定事件,存放事件,触发事件等(店小二,负责通知小明开始按照要求做菜).

SharedEventManager:有时候需要为别的阶段绑定事件.例如在ZF3加载模块的时候要向bootstrap阶段添加事件怎么办?因为bootstrap不仅和加载模块是两个完全独立的事件,而且在加载模块的时候bootstrap事件还不为EventManager所知.这时候就要将事件暂时存放在SharedEventManager中,在稍后EventManager读取bootstrap事件的时候会去查看SharedEventManager中有没有此阶段的逻辑,如果有的话一并触发(如果顾客想给旁边桌的顾客点一道菜,负责将这件事记录下来的本子就是SharedEventManager).

0x03. zend-eventmanager

这一节将从实例化zend-eventmanager模块开始,介绍这一模块的使用方法.

安装:

composer require zendframework/zend-eventmanager

然后在自己的项目中:

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

use Zend\EventManager\EventManager;

$events = new EventManager();

这就得到了zend-eventmanager模块的入口实例.这时就使用其提供的接口,添加,触发事件.

例1:绑定一个输出hello world的匿名函数给一个叫simpleEvent的事件.

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

//触发
$events->trigger('simpleEvent');

//输出:
//hello world

例2:绑定两个匿名函数,一个输出hello,另一个输出XiaoMing,要求两个按照固定顺序执行,即hello Xiaoming.

这时候就涉及到attach()方法的第三个参数,优先级.

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

//按照优先顺序触发
$events->trigger('simpleEvent');

//输出:
//hello XiaoMing

例3:如果我们要向绑定的方法中传入参数怎么办?例如,我们用参数来代替hello后的输出内容.

//绑定并获取默认的Event容器
$events->attach('simpleEvent', function ($event) {
        echo 'hello ' . $event->getParams()['name'];
    }, 100);

//触发,并传入参数
$events->trigger('simpleEvent', null, ['name' => 'Lily']);

//输出
// hello Lily

这里例子中,出现了一个新东西Event,Event是一个容器,用于封装所有Listener(其实就是绑定的那个匿名函数)需要的数据.

具体来说,Event是zend-eventmanager提供的用于封装Listener所需上下文,变量等的一个容器,可以自己定义(稍后介绍),也可以像上文三个例子中的,让zend-eventmanager自动生成一个默认的Event.

默认的Event包括target和params以及name三大部分,target一般用于存放上下文,可以使用getTarget(),setTarget()两个接口来操作,剩下两部分接口类似,都是标准的get,set.params用于存放trigger中的第三个数组参数,以上例为例,是指['name' => 'Lily'],而name则是当前事件的名字,也就是上例中的simpleEvent.

zend-eventmanager在触发绑定Listener的时候会讲Event作为参数传入.

这就是将参数传入Listener中的方法.

trigger中的第二个参数就是target.

例4:例3的另外一种触发方式.自己构建Event,然后触发.

//绑定
$events->attach('simpleEvent', function ($event) {
    //调用自己实现的接口
    echo 'hello ' . $event->getUser();
});

use Zend\EventManager\Event;

//自己构建Event,这是一种偷懒的方法,不推荐,读者请引用
//Zend\EventManager\EventInterface认真构建
class MyEvent extends Event
{
    protected $user;

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

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

$myEvent = new MyEvent;
//设置好事件的名字
$myEvent->setName('simpleEvent');
//自己实现的接口
$myEvent->setUser('Lucy');

//触发
$events->triggerEvent($myEvent);

//输出:
//hello Lucy

这里自己添加了接口getUser()setUser(),用于在Listener中更便捷的访问到变量.

同时还使用了另外一种触发方式triggerEvent(),用于直接触发Event对象.

例5:如果需要从Listener中获取数据怎么办?也就是说Listener的返回值如何接收?

//绑定
$events->attach('simpleEvent', function ($event) {
    //调用自己实现的接口
    return 'hello ' . $event->getUser();
});

use Zend\EventManager\Event;

//自己构建Event,这是一种偷懒的方法,不推荐,读者请引用
//Zend\EventManager\EventInterface认真构建
class MyEvent extends Event
{
    protected $user;

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

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

$myEvent = new MyEvent;
//设置好事件的名字
$myEvent->setName('simpleEvent');
//自己实现的接口
$myEvent->setUser('Lucy');

//触发
$responses = $events->triggerEvent($myEvent);

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

//输出:
//hello Lucy

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

例6:在绑定了多个事件的时候,如何条件的停止事件继续触发?

到目前为止,我们可以顺利的使用zend-eventmanager模块了,但是,一个事件一旦被触发,其Listener将被尽数执行,有没有方法在某种条件下停止执行呢?

zend-eventmanager还提供了两种触发方式triggerUntil()triggerEventUntil(),用于条件停止事件继续执行.

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

//当遇到返回值XiaoMing时停止执行
$responses = $events->triggerUntil(function ($name) {
    if($name === 'XiaoMing')
        return TRUE;
        
    return FALSE;
},'simpleEvent');

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

//输出:
//XiaoMing
//Lucy

由上例可见,最后一个绑定的Listener没有执行.

triggerUntil()triggerEventUntil()两个接口和trigger()以及triggerEvent()分类似,只不过加了第一个参数,一个判断是否停止事件继续执行的匿名函数,传入这个匿名函数的参数是当前Listener的返回值.

另外,Event的stopPropagation()接口被调用也会停止当前事件继续执行.这个比较简单,不做演示.

例7:假设有两个事件event1和event2,event1先执行,如何在event1执行时动态的给event2添加Listener?

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

//实例化SharedEventManager
$shared = new SharedEventManager;

//实例化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,标识为SharedEvent
$events->getSharedManager()->attach('sharedEvent', 'event2', function () { echo 'listener3 from event1 shared'; });

//触发event1
$events->trigger('event1');

//输出:
//listener1 from event1
//listener2 from event1

SharedEventManager刚好可以满足本例需求,SharedEventManager有两个指标来确定一个Listener何时触发,Identifiers事件名,也就是上例中的'sharedEvent'和'event2'.EventManager取得SharedEventManager中的事件时,会对比EventManager当前的Identifiers和所触发的事件名,然后针对的触发Listener.

以下是出发event1为event2绑定的Listener,标识为SharedEvent.

//设置当前EventManager的标识,以取得SharedEventManager对应的事件.
$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的主要用法.

尝试解答那位网友的问题,是不是可以通过事件驱动来解决问题?

是的.业务中遇到较为冗长if...else...时,可以将其中的逻辑全部封装为多个独立的Listener,然后绑定到设计好的多个事件中,然后使用triggerUntil()triggerEventUntil(),传入用于判断的匿名函数.或者根据情况动态的绑定事件.

发表新评论