今年早些时候,我写了关于方面、拦截过滤器、信号槽和事件的文章,目的是比较这些类似的方法来处理异步编程以及以统一的方式处理横切应用程序问题。
我对那篇文章所做的研究进行了研究,并将其应用于ZendFramework2中当时的“SignalSlot”实现,并将该工作重构为一个新的“EventManager”组件。本文旨在帮助您开始使用它。
目录
- 假设
- 术语
- 入门
- EventCollection与EventManager
- 全局静态监听器
- 监听器聚合
- 内省结果
- 短路监听器执行
- 保持有序
- 自定义事件对象
- 放在一起:一个简单的缓存示例
- Fin
- Updates
假设
您必须安装ZendFramework2:
- 来自开发快照(ZF2博客在撰写本文时具有最新链接),或
- 来自克隆ZF2git存储库
术语
- 事件管理器是一个对象,它聚合一个或多个命名事件的侦听器,并触发事件。li>
- Listener是可以对事件做出反应的回调。
- Event是一个动作.
通常,一个事件将被建模为一个对象,包含围绕它何时以及如何被触发的元数据——调用对象是什么,可用的参数是什么,等等。事件通常也被命名,它允许单个侦听器根据当前事件分支逻辑(尽管纯粹主义者会争辩说您永远不应该这样做)。
开始
入门所需的最少的东西是:
EventManager
实例- 一个或多个事件的一个或多个侦听器
- 调用
trigger()一个事件
那么,我们开始吧:
use Zend\EventManager\EventManager; $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函数名、静态类方法、对象实例方法或闭包。我在这篇文章中使用闭包只是为了说明和简化。
注意:在这篇文章中,我使用闭包作为监听器。然而,任何有效的PHP回调都可以作为监听器附加——PHP函数名、静态类方法、对象实例方法或闭包。我在这篇文章中使用闭包只是为了说明和简化。
但是null
,第二个参数是什么?
通常,您将在一个类中编写一个EventManager
,以允许在方法中触发操作。trigger()
的中间参数是“上下文”或“目标”,在所描述的情况下,将是当前对象实例。这使事件侦听器可以访问调用对象,这通常很有用。
use Zend\EventManager\EventCollection, Zend\EventManager\EventManager; class Example { protected $events; public function setEventManager(EventCollection $events) { $this->events = $events; } public function events() { 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()
),并用它做一些事情。
如果您正在批判性地阅读本文,您应该有两个问题:
EventCollection
位是什么?- 传递给
EventManager
构造函数的参数是什么?
第一个的答案将引导我们进入第二个。
EventCollection与EventManager
我们在ZF2中尝试遵循的一个原则是里氏替换原则。对此的一个典型解释是,应该为任何可能存在潜在替换的类定义强接口,以便消费者可以使用其他实现而不必担心内部行为的差异。
因此,我们开发了一个接口EventCollection
,它描述了一个能够聚合事件侦听器并触发这些事件的对象。EventManager
是我们提供的标准实现。
全局静态监听器
EventManager
实现提供的一个方面是与StaticEventCollection
交互的能力。该接口不仅允许在事件上附加侦听器,还允许在特定上下文或目标发出的事件上附加侦听器。EventManager
在通知监听器时,也会从它订阅的StaticEventCollection
对象中提取事件的监听器,并通知他们。
这究竟是如何工作的?
在应用程序级别,您获取StaticEventManager
的实例,并开始将事件附加到它。
use Zend\EventManager\StaticEventManager; $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’目标的‘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()
。这意味着在我们的SubExample
类上调用do()
也会触发我们静态附加的事件!这也意味着,如果需要,我们可以附加到特定的SubExample
,并且不会触发简单的Example
上的侦听器。
最后,用作上下文或目标的名称不必是类名;如果需要,它们可以是一些仅在您的应用程序中有意义的名称。例如,您可以有一组响应“日志”或“缓存”的类——并且它们中的任何一个都会通知这些类的侦听器。
在任何时候,如果您不希望附加到类的EventManager
通知静态附加的侦听器,您可以简单地将null
值传递给setStaticConnections()
方法:
$events->setStaticConnections(null);
它们将被忽略。如果在任何时候,您想再次启用它们,请传递StaticEventManager
实例:
$events->setStaticConnections(StaticEventManager::getInstance());
监听器聚合
通常,您可能希望单个类监听多个事件,将一个或多个实例方法附加为监听器。为了简化此范例,您可以简单地实现HandlerAggregate
接口。此接口定义了两个方法,attach(EventCollection$events)
和detach(EventCollection$events)
。基本上,您将一个EventManager
实例传递给一个和/或另一个,然后由实现类决定要做什么。
举个例子:
use Zend\EventManager\Event, Zend\EventManager\EventCollection, Zend\EventManager\HandlerAggregate, Zend\Log\Logger; 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()
方法中对其进行了聚合。
内省结果
有时您会想知道您的听众返回了什么。要记住的一件事是,同一事件可能有多个侦听器;无论有多少听众,结果的界面都必须保持一致。
默认情况下,EventManager
实现返回一个ResponseCollection
对象。此类扩展了PHP的SplStack
,允许您以相反的顺序循环响应(因为最后执行的响应可能是您最感兴趣的响应)。它还实现了以下方法:
first()
将检索收到的第一个结果last()
将检索收到的最后一个结果contains($value)
允许您测试所有值以查看是否收到给定值,如果找到则返回布尔值true,否则返回false。
通常,您不必担心事件的返回值,因为触发事件的对象不应该真正了解附加的侦听器。但是,有时如果获得有趣的结果,您可能希望短路执行。
短路监听器执行
如果获得特定结果,或者如果侦听器确定出现问题,或者它可以比目标更快地返回某些内容,您可能希望短路执行。
例如,添加EventManager
的一个基本原理是作为一种缓存机制。您可以在方法的早期触发一个事件,如果找到缓存则返回,并在方法的后期触发另一个事件,为缓存做种。
EventManager
组件提供了两种处理方式。第一个是将回调作为最后一个参数传递给trigger()
;打回来;如果该回调返回布尔值true,则执行停止。
这是一个例子:
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 ... }
通过这种范式,我们知道执行暂停的可能原因是由于最后的结果满足测试回调标准;因此,我们只需返回最后的结果。
停止执行的另一种方法是在侦听器中,作用于它接收到的Event
对象。在这种情况下,侦听器调用stopPropagation(true)
,然后EventManager
将返回而不通知任何其他侦听器。
$events->attach('do', function ($e) { $e->stopPropagation(); return new SomeResultClass(); });
当然,这在使用trigger
范例时会产生一些歧义,因为您无法再确定最后的结果是否符合它正在搜索的标准。因此,我的建议是您使用其中一种方法。
保持秩序
有时,您可能会关心侦听器执行的顺序。例如,您可能希望尽早进行任何记录,以确保如果发生短路,您已经记录;或者,如果实现缓存,您可能希望在发现缓存命中时尽早返回,并在保存到缓存时延迟执行。
EventManager::attach()
和StaticEventManager::attach()
都接受一个额外的参数,一个优先级。默认情况下,如果省略它,侦听器的优先级为1,并按照它们被附加的顺序执行。如果您提供优先级值,您可以影响执行顺序。较高优先级的值执行较早,而较低(负)值执行较晚。
借用前面的例子:
$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
,它将以低优先级执行,延迟执行。
虽然您不一定知道所有附加的侦听器,但您可以在必要时进行充分的猜测,以便设置适当的优先级值。我的建议是避免设置优先级值,除非绝对必要。
自定义事件对象
希望你们中的一些人一直在想,“事件对象是在何时何地创建的”?在上面的所有示例中,它都是基于传递给trigger()
的参数创建的——事件名称、目标和参数。然而,有时您可能希望更好地控制对象。
例如,在开发ZF2MVC层时,我们一直在为几个核心MVC组件添加事件感知。当你有这样的代码时,看起来像代码味道的一件事是:
$routeMatch = $e->getParam('route-match', false); if (!$routeMatch) { // Oh noes! we cannot do our work! whatever shall we do?!?!?! }
这有几个问题。首先,依赖字符串键很快就会遇到问题——设置或检索参数时的拼写错误会导致难以调试的情况。其次,我们现在有一个文档问题;我们如何记录预期的参数?我们如何记录我们正在向事件中推进的内容。第三,作为副作用,我们不能使用IDE或编辑器提示支持——字符串键使这些工具无法使用。
同样,我们发现自己编写了一些奇怪的hack,围绕我们如何在触发事件时表示方法的计算结果。例如:
// in the method: $params['__RESULT'] = $computedResult; $events->trigger(__FUNCTION__ . '.post', $this, $params); // in the listener: $result = $e->getParam('__RESULT__'); if (!$result) { // Oh noes! we cannot do our work! whatever shall we do?!?!?! }
当然,该密钥可能是唯一的,但它存在很多相同的问题。
因此,解决方案是创建自定义事件。例如,我们在ZF2MVC层中有一个自定义的“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);
对于特定领域的事件系统来说,这是一种非常强大的技术,绝对值得一试。
综合:一个简单的缓存示例
在上一节中,我指出短路是一种可能实现缓存解决方案的方法。让我们创建一个完整的示例。
首先,让我们定义一个可以使用缓存的方法。您会注意到,在大多数示例中,我使用了__FUNCTION__
作为事件名称;这是一个很好的做法,因为它使创建触发事件的宏变得简单,并且有助于保持事件名称的唯一性(因为它们通常在触发类的上下文中)。但是,在缓存示例的情况下,这会导致触发相同的事件。因此,我建议在事件名称后缀加上语义名称:“do.pre”、“do.post”、“do.error”等。我将在本示例中使用该约定。
此外,您会注意到我传递给事件的$params
通常是传递给方法的参数列表。这是因为那些通常不存储在对象中,并且还要确保侦听器具有与调用方法完全相同的上下文。但这在这个例子中提出了一个有趣的问题:我们给方法的结果起什么名字?我已经对__RESULT__
进行了标准化,因为双下划线变量通常是为系统保留的。如果您有更好的建议,我很乐意听取!
方法如下所示:
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
的范例。如果命中,我们希望早返回被检测到,并在保存缓存时执行late(以防结果被另一个侦听器修改)。因此,我们将设置“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
实例保留为有状态属性,而不是将其导入闭包.
注意:以上可以在
HandlerAggregate
中完成,这将允许将$cache
实例保留为有状态属性,而不是将其导入闭包.
当然,我们可以简单地向对象本身添加缓存-但这种方法允许将相同的处理程序附加到多个事件,或将多个侦听器附加到相同的事件(例如,参数验证器、记录器和缓存管理器)。重点是,如果您在设计对象时考虑到事件,您可以轻松地使其更加灵活和可扩展,而不需要开发人员实际扩展它——他们可以简单地附加监听器。
鳍
EventManager
是ZendFramework的一个强大的新增功能。它已经与新的MVC原型一起使用,以增强一些在版本1.X系列中难以很好地完成的构造——例如,我能够在少数几个代码行,以一种正确实现人们期望从MVC中分离关注点的方式。我预计随着版本2的成熟,我们会更频繁地使用它。
当然有一些粗糙的边缘——用于短路的样板代码很冗长,我们可能想要添加诸如事件通配之类的功能——但在这个时间点基础是坚实和成熟的。试验一下,看看你能完成什么!
更新
- 2011-10-06:删除了对
triggerUntil()
的引用,因为该功能现已合并到trigger()
中。添加了自定义事件对象部分。