前几天看到一个知乎的网友提问如何在业务中避免出现复杂的if…else…逻辑,其中一个答友回答需要去看大型框架的实现.由于个人认为ZF3(ZendFramework3的简写)的事件驱动模块实现的很优雅,有很多值得借鉴的地方,并且恰好解决了这位网友的疑问.
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()
,传入用于判断的匿名函数.或者根据情况动态的绑定事件.