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实现。通过ZendEventManagerListenerAggregateInterface,让一个类监听多个事件,连接一个或多个实例方法作为监听器。同样的该接口也定义了attach(EventManagerInterface $events)和detach(EventManagerInterface $events)。我们在attach的具体实现中使用EventManager的实例的方法attach监听到多个事件。

use ZendEventManagerEventInterface;
use ZendEventManagerEventManagerInterface;
use ZendEventManagerListenerAggregateInterface;
use ZendLogLogger;

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有一个回调函数,该回调函数作为是否进行短回路的依据。

原文地址:https://www.cnblogs.com/san-fu-su/p/5726995.html