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

  • Post author:
  • Post category:其他


前几天看到一个知乎的网友提问如何在业务中避免出现复杂的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()

,传入用于判断的匿名函数.或者根据情况动态的绑定事件.



版权声明:本文为newlooc原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。