本文来自:http://dongbeta.com/2012/02/eventmanager-in-zend-framework-2/
Matthew Weier O'Phinney,资深的PHP工程师。目前在Zend Technologies工作,是Zend Framework的Lead。下面的这篇文章由东至翻译自作者的博客上的Using the ZF2 EventManager。 今年早些时候,我写了一篇关于Aspects, Intercepting Filters, Signal Slots, 和 Events的文章。在这篇文章中我对这三种比较相似的处理异步编程和交叉应用的策略做了比较。 我为了写那篇文章而做的研究后来被应用到了Zend Framework 2中的“SignalSlot”设计中,还有全新的“EventManager”的重构中工作中。现在在这篇文章将做进一步探讨。
use ZendEventManagerEventManager; $events = new EventManager(); $events->attach('do', function($e) { $event = $e->getName(); $params = $e->getParams(); printf( 'Handled event "%s", with parameters %s', $event, json_encode($params) ); }); $params = array('foo' => 'bar', 'baz' => 'bat'); $events->trigger('do', null, $params);上面的代码将会输出:
Handled event "do", with parameters {"foo":"bar","baz":"bat"}很简单吧。
注意:在整篇文章中,我将使用匿名函数作为监听器。实际上,任何有效的PHP回调类型都可以作为监听器使用,比如PHP方法名,静态类的方法,对象的方法还有匿名函数。我在这里使用匿名函数的原因是书写和表达起来比较简单清晰。你可能会注意到一个问题:上面代码中第二个参数“null”是什么? 一般情况下,你会在类的内部构建EventManager,这样它就可以在类成员方法中触发行为。trigger()方法的第二个参数就是”上下文“或者叫做”目标“。所以,在类的内部的话这个参数就应该是当前对象的实例。这样事件监听器就可以调用当前对象。这在实际开发中会很有用。
use ZendEventManagerEventCollection, ZendEventManagerEventManager; class Example { protected $events; public functionsetEventManager(EventCollection $events) { $this->events = $events; } public functionevents() { if (!$this->events) { $this->setEventManager(new EventManager( array(__CLASS__, get_called_class()) ); } return $this->events; } public function do($foo, $baz) { $params = compact('foo', 'baz'); $this->events()->trigger(__FUNCTION__, $this, $params); } } $example = new Example(); $example->events()->attach('do', function($e) { $event = $e->getName(); $target = get_class($e->getTarget()); // "Example" $params = $e->getParams(); printf( 'Handled event "%s" on target "%s", with parameters %s', $event, $target, json_encode($params) ); }); $example->do('bar', 'bat');上面的这例子和第一个几乎一样。主要的不同就是我们现在使用第二个参数来传递上下文到监听器中去。监听器可以使用$e->getTarget()来获取上下文参数。 如果你认真阅读,那么你可能会有下面的疑问:
use ZendEventManagerStaticEventManager; $events = StaticEventManager::getInstance(); $events->attach('Example', 'do', function($e) { $event = $e->getName(); $target = get_class($e->getTarget()); // "Example" $params = $e->getParams(); printf( 'Handled event "%s" on target "%s", with parameters %s', $event, $target, json_encode($params) ); });你应该会注意到这和先前的例子几乎一样。唯一的不同是有个新的参数“Example”。这行代码相当于说:“监听目标‘Example’的‘do’事件,一有通知就执行回调方法”。 这也正是EventManager在执行构造函数的时候所做的事情。构造函数接受一个字符串作为上下文或者目标的名字,也可以是字符串组成的数组。如果是数组,则数组中包含的任何一个目标上所绑定的所有监听器均会被通知。直接绑定到EventManager的监听器会先于静态绑定的监听器执行。 现在,回到例子中。我们假设上面的静态监听器已经注册好了,Example类也定义好了。那么执行下面的代码:
$example = new Example(); $example->do('bar', 'bat');不出意外的话,我们可以得到下面的输出:
Handled event "do" on target "Example", with parameters {"foo":"bar","baz":"bat"}现在,这样扩展Example类:
class SubExample extends Example { }在构造EventManager时的一个需要注意的事情是,我们同时使用了__CLASS__和get_called_class()来定义监听。那么,当调用do()时,SubExample也会触发我们静态绑定的事件!反过来也意味着,如果需要的话,我们可以将事件绑定到指定的子类(如SubExample),而对其父类(如这里的Example)的监听器不起作用。 还有,作为上下文或目标的名字不一定非要是类名,可以是任何有意义的名字。举个例子,你可以定义很多类,这些类可以响应“log”或“cache”事件,其中任何一个事件发生时,这些监听器就可以接到通知。 任何时候,如果你不想某个类内的EventManager通知静态绑定的监听器,你可以简单地传送一个NULL给setStaticConnections()方法。
$events->setStaticConnections(null);这样即可。同理,当你又想启用了,就传一个StaticEventManager的实例即可:
$events->setStaticConnections(StaticEventManager::getInstance());
attach(EventCollection $events)
和
detach(EventCollection $events)。一般情况下,你只需要将一个EventManager的实例传递给这两个方法,然后由你所具体实现的类来决定做什么。
下面是一个例子:
use ZendEventManagerEvent, ZendEventManagerEventCollection, ZendEventManagerHandlerAggregate, ZendLogLogger; class LogEvents implements HandlerAggregate { protected $handlers = array(); protected $log; public function __construct(Logger $log) { $this->log = $log; } public function attach(EventCollection $events) { $this->handlers[] = $events->attach('do', array($this, 'log')); $this->handlers[] = $events->attach('doSomethingElse', array($this, 'log')); } public function detach(EventCollection $events) { foreach ($this->handlers as $key => $handler) { $events->detach($handler); unset($this->handlers[$key]; } $this->handlers = array(); } public function log(Event $e) { $event = $e->getName(); $params = $e->getParams(); $log->info(sprintf('%s: %s', $event, json_encode($params))); } }接下来就可以用下面的代码来进行绑定操作了:
$doLog = new LogEvents($logger); $events->attachAggregate($doLog);然后当某个事件被触发的时候,相应的监听器就可以被通知到。这样,你就可以拥有可记录状态的事件监听器了。 你应该会注意到detach()方法的实现。就像attach()一样,它接受一个EventManager作为参数,然后调用了已经聚合的每个事件句柄的detach方法。这不是错误,因为EventManager::attach()会返回一个对象,这个对象代表一个监听器,也就是我们在聚合器的attach()方法中所聚合的那些。
public function someExpensiveCall($criteria1, $criteria2) { $params = compact('criteria1', 'criteria2'); $results = $this->events()->trigger(__FUNCTION__, $this, $params, function ($r) { return ($r instanceof SomeResultClass); }); if ($results->stopped()) { return $results->last(); } // ... do some work ... }在上面的范例中,我们取得了满足要求的数据,所以中止了执行并直接返回这个数据。 第二种方法是,在监听器内中止执行:直接操作事件对象。这种情况下,监听器应调用事件的stopPropagation(true)方法,然后EventManager就可以返回结果并且不再通知其他相关监听器。
$events->attach('do', function ($e) { $e->stopPropagation(); return new SomeResultClass(); });但是这样又会产生歧义,因为你可能不能确定最后得到的结果是否满足期望。所以,我的建议是使用第一种方法或者另选其他的方法。
$priority = 100; $events->attach('Example', 'do', function($e) { $event = $e->getName(); $target = get_class($e->getTarget()); // "Example" $params = $e->getParams(); printf( 'Handled event "%s" on target "%s", with parameters %s', $event, $target, json_encode($params) ); }, $priority);上面的代码中设置了一个高权重,意味着这个监听器会优先执行。如果将$priority改为-100,则执行权重降低也就是后执行。 当你不能确切知道所有绑定的监听器时,只能靠自己的推断来进行权重的设置。我的建议是能不设置权重时就不设置。
$routeMatch = $e->getParam('route-match', false); if (!$routeMatch) { // Oh noes! we cannot do our work! whatever shall we do?!?!?! }这样就有了几个问题。首先,字符串形式的参数名容易出错——比如在设置或者检索参数时的拼写错误,而且这种错误很难排查。其次,如何写文档也是个问题——我们如何描述传递给事件的参数呢?再次,一个副作用,就是我们无法使用IDE或编辑器的拼写提示了——它们无法识别字符串参数作为关键词。 类似地,我们发现在触发一个事件时,关于如何表达一个方法的计算结果我们老是在做hack。举个例子:
// 在方法中: $params['__RESULT'] = $computedResult; $events->trigger(__FUNCTION__ . '.post', $this, $params); // 在监听器里: $result = $e->getParam('__RESULT__'); if (!$result) { // Oh noes! we cannot do our work! whatever shall we do?!?!?! }当然,这个键名有点特殊,但是确实有很多类似的问题存在。 所以,解决方法就是创建自定义事件。举个例子,我们在ZF2的MVC层创建了一个自定义的类“MvcEvent”。这个事件组成一个路由,路由匹配对象,请求和响应对象,返回一个结果对象。最后我们写出了下面的代码:
$response = $e->getResponse(); $result = $e->getResult(); if (is_string($result)) { $content = $view->render('layout.phtml', array('content' => $result)); $response->setContent($content); }我们如何使用这个自定义的事件对象呢?很简单:trigger()方法接受的事件名,目标或配置参数都可以用对象来代替。
$event = new CustomEvent(); $event->setSomeKey($value); // Injected with event name and target: $events->trigger('foo', $this, $event); // Injected with event name: $event->setTarget($this); $events->trigger('foo', $event); // Fully encapsulates all necessary properties: $event->setName('foo'); $event->setTarget($this); $events->trigger($event); // Passing a callback following the event object works for // short-circuiting, too. $results = $events->trigger('foo', $this, $event, $callback);对于与域相关的对象系统,这确实是很强大的技术,值得去尝试。
public function someExpensiveCall($criteria1, $criteria2) { $params = compact('criteria1', 'criteria2'); $results = $this->events()->trigger(__FUNCTION__ . '.pre', $this, $params, function ($r) { return ($r instanceof SomeResultClass); }); if ($results->stopped()) { return $results->last(); } // ... do some work ... $params['__RESULT__'] = $calculatedResult; $this->events()->trigger(__FUNCTION__ . '.post', $this, $params); return $calculatedResult; }现在来添加一些缓存监听器。我们需要给每一个'someExpensiveCall.pre'和'someExpensiveCall.post'绑定监听器。在之前的例子中,如果命中缓存,则返回缓存数据然后继续运行。缓存数据的运行放在后面。 先假定$cache已经定义好了,并且遵循Zend_Cache范式。我们需要在缓存命中之后尽快将其返回,而保存缓存数据的操作要尽可能放到后面(因为可能会还有其他监听器存在)。所以,我们设置'someExpensiveCall.pre'的权重为100,而'someExpensiveCall.post'的是-100.
$events->attach('someExpensiveCall.pre', function($e) use ($cache) { $params = $e->getParams(); $key = md5(json_encode($params)); $hit = $cache->load($key); return $hit; }, 100); $events->attach('someExpensiveCall.post', function($e) use ($cache) { $params = $e->getParams(); $result = $params['__RESULT__']; unset($params['__RESULT__']); $key = md5(json_encode($params)); $cache->save($result, $key); }, -100);
注意,你也可以使用HandlerAggregate,这样可以将$cache作为一个属性使用而不是传递给匿名函数。当然,我们本来可以只简单地给对象添加缓存功能,但是现在的机制可以允许将同样的处理机制绑定到多个事件中,或者绑定多个监听器到一个事件(比如参数验证器,日志和缓存管理)。所以,如果你按照事件驱动的思路去设计一个对象,你可以获得更强大的灵活性和扩展性而不需要开发者真的去继承一个类——只需要加几个监听器即可。