zendframework 事件管理(二)
首先需要明确的几个问题:
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实现。通过Zend\EventManager\ListenerAggregateInterface,让一个类监听多个事件,连接一个或多个实例方法作为监听器。同样的该接口也定义了attach(EventManagerInterface $events)和detach(EventManagerInterface $events)。我们在attach的具体实现中使用EventManager的实例的方法attach监听到多个事件。
use Zend\EventManager\EventInterface; use Zend\EventManager\EventManagerInterface; use Zend\EventManager\ListenerAggregateInterface; use Zend\Log\Logger; class LogEvents implements ListenerAggregateInterface { private $listeners = []; private $log; public function __construct(Logger $log) { $this->log = $log; } public function attach(EventManagerInterface $events) { $this->listeners[] = $events->attach('do', [$this, 'log']); $this->listeners[] = $events->attach('doSomethingElse', [$this, 'log']); } public function detach(EventCollection $events) { foreach ($this->listeners as $index => $listener) { $events->detach($listener); unset($this->listeners[$index]); } } public function log(EventInterface $e) { $event = $e->getName(); $params = $e->getParams(); $this->log->info(sprintf('%s: %s', $event, json_encode($params))); } }
使用Aggregate的好处:
1、允许你使用有状态的监听器
2、在单一的类中组合多个相近的监听器,并一次性连接他们
内省监听器返回的结果
我们有了监听器,但如何接收他返回的结果呢?EventManager默认实现会返回一个ResponseCollection的实例。这个类继承于PHP的SplStack。基本结构是一个栈,所以允许你反序遍历Responses。
ResponseCollection提供了有用的几个方法:
first(): 获取第一个结果
last(): 获取最后一个结果
contains($value): 查看是否栈里面是否包含某一个值,如果包含则返回true,否则false。
短回路监听器执行:
什么叫短回路呢?假设你要做一件事情,直到这件事有了结果,这是一个回路。如果你提前知道了这件事的结果(比如之前做过这件事),那你就没比要把这件事完完全全的做完,这时候你只需要执行一个短回路。
我们在添加EventManager的时候有一个缓存机制。在一个方法中触发一个事件,如果我们找到一个缓存结果就直接返回。如果找不到缓存结果,我们就将触发的事件缓存下来以备后用。实际上和计算机硬件里面的高速缓存一个道理。
EventManager组件提供两种处理的方式:1、triggerUntil();2、triggerEventUntil。这两个方法都接受一个回调函数作为第一个参数。如果回调函数返回true,那执行停止。
public function someExpensiveCall($criteria1, $criteria2) { $params = compact('criteria1', 'criteria2'); $results = $this->getEventManager()->triggerUntil( function($r){ return ($r instanceof SomeResultClass); }, __FUNCTION__, $this, $params ); if($results->stopped()) { return $results->last()' } }
从上面范例中,我们知道,如果执行停止了很有可能是因为栈里面最后的结果满足我们的要求。这样一来,我们只要返回该结果,何必还要进行多余的计算呢?
处理在事件中停止执行,我们还可以在监听器中停止执行。理由是我们曾经接收过某一个事件,现在我们又接收到了相同事件,理所当然的使用之前的结果就好了。这种情况下,监听器调用stopPropagation(true),然后EventManager会直接返回而不会继续通知额外的监听器。
$events->attach('do', function($e) { $e->stopPropagation(); return new SomeResultClass(); });
当然,使用触发器范例可能会导致歧义,毕竟你并不知道最终的结果是否满足要求。
Keeping it in order.
偶尔你会关心监听器的执行顺序。我们通过监听器的优先级来控制执行顺序(上面说讲的短回路也会影响执行顺序)。每一个EventManager::attach()和SharedEventManager::attach()都会接受一个而外的参数:priority。默认情况下为1,我们可以省略该参数。如果你提供了该参数:高优先级执行的早,低优先级的可能会推迟执行。
自定义事件对象:
我们之前使用trigger()触发事件,在这同时我们也创建了事件。但trigger()的参数有限,我们只能指定事件的对象,参数,名称。实际上我们可以创建一个自定义事件,在Zendframework里面有个很重要的事件:MvcEvent。很显然MvcEvent便是一个自定义事件,该事件组合了application实例,路由器,路由匹配对象,请求和应答对象,视图模型还有结果。我们查看MvcEvent的源码会发现MvcEvent类实际上继承了Event类。同理我们的自定义事件对象也可以继承Event类或者继承MvcEvent。
$event = new CustomEvent(); $event->setName('foo'); $event->setTarget($this); $event->setSomeKey($value); //injected with event name and target: $events->triggerEvent($event); //Use triggerEventUntil() for criteria-based short-circuiting: $results = $events->triggerEventUntil($callback, $event);
上面的代码可以看到我们使用自定义事件类创建了一个事件对象,调用相关拦截器为事件对象设置属性。我们有了事件对象还是用trigger()触发事件吗?显然不是,我们使用triggerEvent($event)方法,该方法接收一个事件对象。而triggerEventUntil有一个回调函数,该回调函数作为是否进行短回路的依据。