mirror of
https://github.com/stan220/WildcardEventDispatcher.git
synced 2026-09-08 15:28:37 +00:00
First commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
phpunit.xml
|
||||||
|
vendor
|
||||||
|
composer.lock
|
||||||
|
composer.phar
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
Copyright (c) 2012 Jeremy Mikola
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# WildcardEventDispatcher
|
||||||
|
|
||||||
|
This library implements an event dispatcher, based on [Symfony2's interface][],
|
||||||
|
with wildcard syntax inspired by AMQP topic exchanges. Listeners may be bound to
|
||||||
|
a wildcard pattern and be notified if a dispatched event's name matches that
|
||||||
|
pattern. Literal event name matching is still supported.
|
||||||
|
|
||||||
|
[Symfony2's interface]: https://github.com/symfony/EventDispatcher
|
||||||
|
|
||||||
|
## Usage ##
|
||||||
|
|
||||||
|
WildcardEventDispatcher implements EventDispatcherInterface and may be used as
|
||||||
|
you would Symfony2's standard EventDispatcher:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Jmikola\WildcardEventDispatcher\WildcardEventDispatcher;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
|
$dispatcher = new WildcardEventDispatcher();
|
||||||
|
$dispatcher->addListener('core.*', function(Event $e) {
|
||||||
|
echo $e->getName();
|
||||||
|
});
|
||||||
|
$dispatcher->dispatch('core.request');
|
||||||
|
|
||||||
|
// "core.request" will be printed
|
||||||
|
```
|
||||||
|
|
||||||
|
Internally, WildcardEventDispatcher actually [composes][] an
|
||||||
|
EventDispatcherInterface instance, which it relies upon for event handling. By
|
||||||
|
default, WildcardEventDispatcher will construct an EventDispatcher object for
|
||||||
|
internal use, but you may specify a particular EventDispatcherInterface instance
|
||||||
|
to wrap in the constructor:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Jmikola\WildcardEventDispatcher\WildcardEventDispatcher;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
|
|
||||||
|
$dispatcher = new WildcardEventDispatcher(new EventDispatcher());
|
||||||
|
```
|
||||||
|
|
||||||
|
[composes]: http://en.wikipedia.org/wiki/Object_composition
|
||||||
|
|
||||||
|
## Wildcard Syntax ##
|
||||||
|
|
||||||
|
### Single-word Wildcard ###
|
||||||
|
|
||||||
|
Consider the scenario where the same listener is defined for multiple events,
|
||||||
|
all of which share a common prefix:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$coreListener = function(Event $e) {};
|
||||||
|
|
||||||
|
$dispatcher = new WildcardEventDispatcher();
|
||||||
|
$dispatcher->addListener('core.exception', $coreListener);
|
||||||
|
$dispatcher->addListener('core.request', $coreListener);
|
||||||
|
$dispatcher->addListener('core.response', $coreListener);
|
||||||
|
```
|
||||||
|
|
||||||
|
These event names all consist of two dot-separated words. This concept of a word
|
||||||
|
will be important in understanding how wildcard patterns apply.
|
||||||
|
|
||||||
|
In this example, the listener is responsible for observing all `core` events in
|
||||||
|
the application. Let's suppose it needs to log some details about these events
|
||||||
|
to an external server. We can refactor multiple `addListener()` calls by
|
||||||
|
using the single-word `*` wildcard:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$coreListener = function(Event $e) {};
|
||||||
|
|
||||||
|
$dispatcher = new WildcardEventDispatcher();
|
||||||
|
$dispatcher->addListener('core.*', $coreListener);
|
||||||
|
```
|
||||||
|
|
||||||
|
The listener will now observe all events named `core` or starting with `core.`
|
||||||
|
and followed by another word. The matching of `core` alone may not make sense,
|
||||||
|
but this is implemented in order to be consistent with AMQP. A trailing `*`
|
||||||
|
after a non-empty sequence may match the preceding sequence sans `.*`.
|
||||||
|
|
||||||
|
### Multi-word Wildcard ###
|
||||||
|
|
||||||
|
Suppose there was a `core` event in your application named `core.foo.bar`. The
|
||||||
|
aforementioned `core.*` pattern would not catch this event. You could use:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$coreListener = function(Event $e) {};
|
||||||
|
|
||||||
|
$dispatcher = new WildcardEventDispatcher();
|
||||||
|
$dispatcher->addListener('core.*.*', $coreListener);
|
||||||
|
```
|
||||||
|
|
||||||
|
This syntax would match `core.foo` and `core.foo.bar`, but `core` would no
|
||||||
|
longer be matched (assuming there was such an event).
|
||||||
|
|
||||||
|
The multi-word `#` wildcard might be more appropriate here:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$coreListener = function(Event $e) {};
|
||||||
|
|
||||||
|
$dispatcher = new WildcardEventDispatcher();
|
||||||
|
$dispatcher->addListener('core.#', $coreListener);
|
||||||
|
```
|
||||||
|
|
||||||
|
Suppose there was also an listener in the application that needed to listen on
|
||||||
|
_all_ events in the application. The multi-word `#` wildcard could be used:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$allListener = function(Event $e) {};
|
||||||
|
|
||||||
|
$dispatcher = new WildcardEventDispatcher();
|
||||||
|
$dispatcher->addListener('#', $allListener);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Wildcard Documentation ###
|
||||||
|
|
||||||
|
When in doubt, the unit tests for `ListenerPattern` are a good resource for
|
||||||
|
inferring how wildcards will be interpreted. This library aims to mimic the
|
||||||
|
behavior of AMQP topic wildcards completely, but there may be shortcomings.
|
||||||
|
|
||||||
|
Documentation for actual AMQP syntax may be found in the following packages:
|
||||||
|
|
||||||
|
* [ActiveMQ](http://activemq.apache.org/wildcards.html)
|
||||||
|
* [HornetQ](http://docs.jboss.org/hornetq/2.2.5.Final/user-manual/en/html/wildcard-syntax.html)
|
||||||
|
* [RabbitMQ](http://www.rabbitmq.com/faq.html#wildcards-in-topic-exchanges)
|
||||||
|
* [ZeroMQ](http://www.zeromq.org/whitepapers:message-matching)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "jmikola/wildcard-event-dispatcher",
|
||||||
|
"type": "library",
|
||||||
|
"description": "Event dispatcher with support for wildcard patterns inspired by AMQP topic exchanges.",
|
||||||
|
"keywords": ["AMQP", "dispatcher", "event", "event dispatcher"],
|
||||||
|
"homepage": "https://github.com/jmikola/WildcardEventDispatcher",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{ "name": "Jeremy Mikola", "email": "jmikola@gmail.com" }
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.3.2",
|
||||||
|
"symfony/event-dispatcher": ">=2.0-dev,<2.2-dev"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": { "Jmikola\\WildcardEventDispatcher": "" }
|
||||||
|
},
|
||||||
|
"target-dir": "Jmikola/WildcardEventDispatcher"
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit bootstrap="./tests/bootstrap.php" colors="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="WildcardEventDispatcher Test Suite">
|
||||||
|
<directory>./tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./tests</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Jmikola\WildcardEventDispatcher;
|
||||||
|
|
||||||
|
class LazyListenerPattern extends ListenerPattern
|
||||||
|
{
|
||||||
|
protected $listenerProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* The $listenerProvider argument should be a callback which, when invoked,
|
||||||
|
* returns the listener callback.
|
||||||
|
*
|
||||||
|
* @param string $eventPattern
|
||||||
|
* @param callback $listenerProvider
|
||||||
|
* @param integer $priority
|
||||||
|
* @throws InvalidArgumentException if the listener provider is not a callback
|
||||||
|
*/
|
||||||
|
public function __construct($eventPattern, $listenerProvider, $priority = 0)
|
||||||
|
{
|
||||||
|
if (!is_callable($listenerProvider)) {
|
||||||
|
throw new \InvalidArgumentException('Listener provider argument must be a callback');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($eventPattern, null, $priority);
|
||||||
|
|
||||||
|
$this->listenerProvider = $listenerProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the pattern listener, initializing it lazily from its provider.
|
||||||
|
*
|
||||||
|
* @return callback
|
||||||
|
*/
|
||||||
|
public function getListener()
|
||||||
|
{
|
||||||
|
if (!isset($this->listener) && isset($this->listenerProvider)) {
|
||||||
|
$this->listener = call_user_func($this->listenerProvider);
|
||||||
|
unset($this->listenerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Jmikola\WildcardEventDispatcher;
|
||||||
|
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
|
class ListenerPattern
|
||||||
|
{
|
||||||
|
protected $eventPattern;
|
||||||
|
protected $events = array();
|
||||||
|
protected $listener;
|
||||||
|
protected $priority;
|
||||||
|
protected $regex;
|
||||||
|
|
||||||
|
private static $replacements = array(
|
||||||
|
// Trailing single-wildcard with separator prefix
|
||||||
|
'/\\\\\.\\\\\*$/' => '(?:\.\w+)?',
|
||||||
|
// Single-wildcard with separator prefix
|
||||||
|
'/\\\\\.\\\\\*/' => '(?:\.\w+)',
|
||||||
|
// Single-wildcard without separator prefix
|
||||||
|
'/(?<!\\\\\.)\\\\\*/' => '(?:\w+)',
|
||||||
|
// Multi-wildcard with separator prefix
|
||||||
|
'/\\\\\.#/' => '(?:\.\w+)*',
|
||||||
|
// Multi-wildcard without separator prefix
|
||||||
|
'/(?<!\\\\\.)#/' => '(?:|\w+(?:\.\w+)*)',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param string $eventPattern
|
||||||
|
* @param callback $listener
|
||||||
|
* @param integer $priority
|
||||||
|
*/
|
||||||
|
public function __construct($eventPattern, $listener, $priority = 0)
|
||||||
|
{
|
||||||
|
$this->eventPattern = $eventPattern;
|
||||||
|
$this->listener = $listener;
|
||||||
|
$this->priority = $priority;
|
||||||
|
$this->regex = $this->createRegex($eventPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the event pattern.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getEventPattern()
|
||||||
|
{
|
||||||
|
return $this->eventPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the listener.
|
||||||
|
*
|
||||||
|
* @return callback
|
||||||
|
*/
|
||||||
|
public function getListener()
|
||||||
|
{
|
||||||
|
return $this->listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds this pattern's listener to an event.
|
||||||
|
*
|
||||||
|
* @param EventDispatcherInterface $dispatcher
|
||||||
|
* @param string $eventName
|
||||||
|
*/
|
||||||
|
public function bind(EventDispatcherInterface $dispatcher, $eventName)
|
||||||
|
{
|
||||||
|
if (isset($this->events[$eventName])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->addListener($eventName, $this->getListener(), $this->priority);
|
||||||
|
$this->events[$eventName] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes this pattern's listener from all events to which it was
|
||||||
|
* previously added.
|
||||||
|
*
|
||||||
|
* @param EventDispatcherInterface $dispatcher
|
||||||
|
*/
|
||||||
|
public function unbind(EventDispatcherInterface $dispatcher)
|
||||||
|
{
|
||||||
|
foreach ($this->events as $eventName => $_) {
|
||||||
|
$dispatcher->removeListener($eventName, $this->getListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->events = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if this pattern matches and event name.
|
||||||
|
*
|
||||||
|
* @param string $eventName
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public final function test($eventName)
|
||||||
|
{
|
||||||
|
return (boolean) preg_match($this->regex, $eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms an event pattern into a regular expression.
|
||||||
|
*
|
||||||
|
* @param string $eventPattern
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function createRegex($eventPattern)
|
||||||
|
{
|
||||||
|
return sprintf('/^%s$/', preg_replace(
|
||||||
|
array_keys(self::$replacements),
|
||||||
|
array_values(self::$replacements),
|
||||||
|
preg_quote($eventPattern, '/')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Jmikola\WildcardEventDispatcher;
|
||||||
|
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
|
class WildcardEventDispatcher implements EventDispatcherInterface
|
||||||
|
{
|
||||||
|
private $dispatcher;
|
||||||
|
private $patterns = array();
|
||||||
|
private $syncedEvents = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* If an EventDispatcherInterface is not provided , a new EventDispatcher
|
||||||
|
* will be composed.
|
||||||
|
*
|
||||||
|
* @param EventDispatcherInterface $dispatcher
|
||||||
|
*/
|
||||||
|
public function __construct(EventDispatcherInterface $dispatcher = null)
|
||||||
|
{
|
||||||
|
$this->dispatcher = $dispatcher ?: new EventDispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see EventDispatcherInterface::dispatch()
|
||||||
|
*/
|
||||||
|
public function dispatch($eventName, Event $event = null)
|
||||||
|
{
|
||||||
|
$this->bindPatterns($eventName);
|
||||||
|
|
||||||
|
return $this->dispatcher->dispatch($eventName, $event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see EventDispatcherInterface::getListeners()
|
||||||
|
*/
|
||||||
|
public function getListeners($eventName = null)
|
||||||
|
{
|
||||||
|
if (null !== $eventName) {
|
||||||
|
$this->bindPatterns($eventName);
|
||||||
|
|
||||||
|
return $this->dispatcher->getListeners($eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure that any patterns matching a known event name are bound. If
|
||||||
|
* we don't this this, it's possible that getListeners() could return
|
||||||
|
* different values due to lazy listener registration.
|
||||||
|
*/
|
||||||
|
foreach (array_keys($this->dispatcher->getListeners()) as $eventName) {
|
||||||
|
$this->bindPatterns($eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dispatcher->getListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see EventDispatcherInterface::hasListeners()
|
||||||
|
*/
|
||||||
|
public function hasListeners($eventName = null)
|
||||||
|
{
|
||||||
|
return (boolean) count($this->getListeners($eventName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see EventDispatcherInterface::addListener()
|
||||||
|
*/
|
||||||
|
public function addListener($eventName, $listener, $priority = 0)
|
||||||
|
{
|
||||||
|
return $this->hasWildcards($eventName)
|
||||||
|
? $this->addListenerPattern(new ListenerPattern($eventName, $listener, $priority))
|
||||||
|
: $this->dispatcher->addListener($eventName, $listener, $priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see EventDispatcherInterface::removeListener()
|
||||||
|
*/
|
||||||
|
public function removeListener($eventName, $listener)
|
||||||
|
{
|
||||||
|
return $this->hasWildcards($eventName)
|
||||||
|
? $this->removeListenerPattern($eventName, $listener)
|
||||||
|
: $this->dispatcher->removeListener($eventName, $listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see EventDispatcherInterface::addSubscriber()
|
||||||
|
*/
|
||||||
|
public function addSubscriber(EventSubscriberInterface $subscriber)
|
||||||
|
{
|
||||||
|
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
|
||||||
|
if (is_array($params)) {
|
||||||
|
$this->addListener($eventName, array($subscriber, $params[0]), $params[1]);
|
||||||
|
} else {
|
||||||
|
$this->addListener($eventName, array($subscriber, $params));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see EventDispatcherInterface::removeSubscriber()
|
||||||
|
*/
|
||||||
|
public function removeSubscriber(EventSubscriberInterface $subscriber)
|
||||||
|
{
|
||||||
|
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
|
||||||
|
$this->removeListener($eventName, array($subscriber, is_array($params) ? $params[0] : $params));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a string contains any wildcard characters.
|
||||||
|
*
|
||||||
|
* @param string $subject
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
protected function hasWildcards($subject)
|
||||||
|
{
|
||||||
|
return false !== strpos($subject, '*') || false !== strpos($subject, '#');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds all patterns that match the specified event name.
|
||||||
|
*
|
||||||
|
* @param string $eventName
|
||||||
|
*/
|
||||||
|
protected function bindPatterns($eventName)
|
||||||
|
{
|
||||||
|
if (isset($this->syncedEvents[$eventName])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->patterns as $eventPattern => $patterns) {
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if ($pattern->test($eventName)) {
|
||||||
|
$pattern->bind($this->dispatcher, $eventName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->syncedEvents[$eventName] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an event listener for all events matching the specified pattern.
|
||||||
|
*
|
||||||
|
* This method will lazily register the listener when a matching event is
|
||||||
|
* dispatched.
|
||||||
|
*
|
||||||
|
* @param ListenerPattern $pattern
|
||||||
|
*/
|
||||||
|
protected function addListenerPattern(ListenerPattern $pattern)
|
||||||
|
{
|
||||||
|
$this->patterns[$pattern->getEventPattern()][] = $pattern;
|
||||||
|
|
||||||
|
foreach ($this->syncedEvents as $eventName => $_) {
|
||||||
|
if ($pattern->test($eventName)) {
|
||||||
|
unset($this->syncedEvents[$eventName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an event listener from any events to which it was applied due to
|
||||||
|
* pattern matching.
|
||||||
|
*
|
||||||
|
* This method cannot be used to remove a listener from a pattern that was
|
||||||
|
* never registered.
|
||||||
|
*
|
||||||
|
* @param string $eventPattern
|
||||||
|
* @param callback $listener
|
||||||
|
*/
|
||||||
|
protected function removeListenerPattern($eventPattern, $listener)
|
||||||
|
{
|
||||||
|
if (!isset($this->patterns[$eventPattern])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->patterns[$eventPattern] as $key => $pattern) {
|
||||||
|
if ($listener == $pattern->getListener()) {
|
||||||
|
$pattern->unbind($this->dispatcher);
|
||||||
|
unset($this->patterns[$eventPattern][$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Jmikola\Tests\WildcardEventDispatcher;
|
||||||
|
|
||||||
|
use Jmikola\WildcardEventDispatcher\LazyListenerPattern;
|
||||||
|
|
||||||
|
class LazyListenerPatternTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @expectedException InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testConstructorRequiresListenerProviderCallback()
|
||||||
|
{
|
||||||
|
new LazyListenerPattern('*', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLazyListenerInitialization()
|
||||||
|
{
|
||||||
|
$listenerProviderInvoked = 0;
|
||||||
|
|
||||||
|
$listenerProvider = function() use (&$listenerProviderInvoked) {
|
||||||
|
++$listenerProviderInvoked;
|
||||||
|
return 'callback';
|
||||||
|
};
|
||||||
|
|
||||||
|
$pattern = new LazyListenerPattern('*', $listenerProvider);
|
||||||
|
|
||||||
|
$this->assertEquals(0, $listenerProviderInvoked, 'The listener provider should not be invoked until the listener is requested');
|
||||||
|
$this->assertEquals('callback', $pattern->getListener());
|
||||||
|
$this->assertEquals(1, $listenerProviderInvoked, 'The listener provider should be invoked when the listener is requested');
|
||||||
|
$this->assertEquals('callback', $pattern->getListener());
|
||||||
|
$this->assertEquals(1, $listenerProviderInvoked, 'The listener provider should only be invoked once');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Jmikola\Tests\WildcardEventDispatcher;
|
||||||
|
|
||||||
|
use Jmikola\WildcardEventDispatcher\ListenerPattern;
|
||||||
|
|
||||||
|
class ListenerPatternTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider providePatternsAndMatches
|
||||||
|
*/
|
||||||
|
public function testPatternMatching($eventPattern, array $expectedMatches, array $expectedMisses)
|
||||||
|
{
|
||||||
|
$pattern = new ListenerPattern($eventPattern, null);
|
||||||
|
|
||||||
|
foreach ($expectedMatches as $eventName) {
|
||||||
|
$this->assertTrue($pattern->test($eventName), sprintf('Pattern "%s" should match event "%s"', $eventPattern, $eventName));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($expectedMisses as $eventName) {
|
||||||
|
$this->assertFalse($pattern->test($eventName), sprintf('Pattern "%s" should not match event "%s"', $eventPattern, $eventName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function providePatternsAndMatches()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
array(
|
||||||
|
'*',
|
||||||
|
array('core', 'api', 'v2'),
|
||||||
|
array('', 'core.request'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'*.exception',
|
||||||
|
array('core.exception', 'api.exception'),
|
||||||
|
array('core', 'api.exception.internal'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'core.*',
|
||||||
|
array('core', 'core.request', 'core.v2'),
|
||||||
|
array('api', 'core.exception.internal'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'api.*.*',
|
||||||
|
array('api.exception', 'api.exception.internal'),
|
||||||
|
array('api', 'core'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'#',
|
||||||
|
array('core', 'core.request', 'api.exception.internal', 'api.v2'),
|
||||||
|
array(),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'api.#.created',
|
||||||
|
array('api.created', 'api.user.created', 'api.v2.user.created'),
|
||||||
|
array('core.created', 'core.user.created', 'core.api.user.created'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'api.*.cms.#',
|
||||||
|
array('api.v2.cms', 'api.v2.cms.post', 'api.v2.cms.post.created'),
|
||||||
|
array('api.v2', 'core.request.cms'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'api.#.post.*',
|
||||||
|
array('api.post', 'api.post.created', 'api.v2.cms.post.created'),
|
||||||
|
array('api', 'api.user', 'core.api.post.created'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDispatcherBinding()
|
||||||
|
{
|
||||||
|
$pattern = new ListenerPattern('core.*', $listener = 'callback', $priority = 0);
|
||||||
|
|
||||||
|
$dispatcher = $this->getMockEventDispatcher();
|
||||||
|
|
||||||
|
$dispatcher->expects($this->once())
|
||||||
|
->method('addListener')
|
||||||
|
->with('core.request', $listener, $priority);
|
||||||
|
|
||||||
|
$pattern->bind($dispatcher, 'core.request');
|
||||||
|
|
||||||
|
// bind() should avoid adding the listener multiple times to the same event
|
||||||
|
$pattern->bind($dispatcher, 'core.request');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMockEventDispatcher()
|
||||||
|
{
|
||||||
|
return $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
|
||||||
|
}
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Jmikola\Tests\WildcardEventDispatcher;
|
||||||
|
|
||||||
|
use Jmikola\WildcardEventDispatcher\WildcardEventDispatcher;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
|
class WildcardEventDispatcherFunctionalTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
const coreRequest = 'core.request';
|
||||||
|
const coreException = 'core.exception';
|
||||||
|
const apiRequest = 'api.request';
|
||||||
|
const apiException = 'api.exception';
|
||||||
|
|
||||||
|
private $dispatcher;
|
||||||
|
private $listener;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->dispatcher = new WildcardEventDispatcher();
|
||||||
|
$this->listener = new TestEventListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInitialState()
|
||||||
|
{
|
||||||
|
$this->assertEquals(array(), $this->dispatcher->getListeners());
|
||||||
|
$this->assertFalse($this->dispatcher->hasListeners(self::coreRequest));
|
||||||
|
$this->assertFalse($this->dispatcher->hasListeners(self::coreException));
|
||||||
|
$this->assertFalse($this->dispatcher->hasListeners(self::apiRequest));
|
||||||
|
$this->assertFalse($this->dispatcher->hasListeners(self::apiException));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddingAndRemovingListeners()
|
||||||
|
{
|
||||||
|
$this->dispatcher->addListener('#', array($this->listener, 'onAny'));
|
||||||
|
$this->dispatcher->addListener('core.*', array($this->listener, 'onCore'));
|
||||||
|
$this->dispatcher->addListener('*.exception', array($this->listener, 'onException'));
|
||||||
|
$this->dispatcher->addListener(self::coreRequest, array($this->listener, 'onCoreRequest'));
|
||||||
|
|
||||||
|
$this->assertNumberListenersAdded(3, self::coreRequest);
|
||||||
|
$this->assertNumberListenersAdded(3, self::coreException);
|
||||||
|
$this->assertNumberListenersAdded(1, self::apiRequest);
|
||||||
|
$this->assertNumberListenersAdded(2, self::apiException);
|
||||||
|
$this->assertNumberListenersAdded(9);
|
||||||
|
|
||||||
|
$this->dispatcher->removeListener('#', array($this->listener, 'onAny'));
|
||||||
|
|
||||||
|
$this->assertNumberListenersAdded(2, self::coreRequest);
|
||||||
|
$this->assertNumberListenersAdded(2, self::coreException);
|
||||||
|
$this->assertNumberListenersAdded(0, self::apiRequest);
|
||||||
|
$this->assertNumberListenersAdded(1, self::apiException);
|
||||||
|
$this->assertNumberListenersAdded(5);
|
||||||
|
|
||||||
|
$this->dispatcher->removeListener('core.*', array($this->listener, 'onCore'));
|
||||||
|
|
||||||
|
$this->assertNumberListenersAdded(1, self::coreRequest);
|
||||||
|
$this->assertNumberListenersAdded(1, self::coreException);
|
||||||
|
$this->assertNumberListenersAdded(0, self::apiRequest);
|
||||||
|
$this->assertNumberListenersAdded(1, self::apiException);
|
||||||
|
$this->assertNumberListenersAdded(3);
|
||||||
|
|
||||||
|
$this->dispatcher->removeListener('*.exception', array($this->listener, 'onException'));
|
||||||
|
|
||||||
|
$this->assertNumberListenersAdded(1, self::coreRequest);
|
||||||
|
$this->assertNumberListenersAdded(0, self::coreException);
|
||||||
|
$this->assertNumberListenersAdded(0, self::apiRequest);
|
||||||
|
$this->assertNumberListenersAdded(0, self::apiException);
|
||||||
|
$this->assertNumberListenersAdded(1);
|
||||||
|
|
||||||
|
$this->dispatcher->removeListener(self::coreRequest, array($this->listener, 'onCoreRequest'));
|
||||||
|
|
||||||
|
$this->assertNumberListenersAdded(0, self::coreRequest);
|
||||||
|
$this->assertNumberListenersAdded(0, self::coreException);
|
||||||
|
$this->assertNumberListenersAdded(0, self::apiRequest);
|
||||||
|
$this->assertNumberListenersAdded(0, self::apiException);
|
||||||
|
$this->assertNumberListenersAdded(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddedListenersWithWildcardsAreRegisteredLazily()
|
||||||
|
{
|
||||||
|
$this->dispatcher->addListener('#', array($this->listener, 'onAny'));
|
||||||
|
|
||||||
|
$this->assertNumberListenersAdded(0);
|
||||||
|
|
||||||
|
$this->assertTrue($this->dispatcher->hasListeners(self::coreRequest));
|
||||||
|
$this->assertNumberListenersAdded(1, self::coreRequest);
|
||||||
|
$this->assertNumberListenersAdded(1);
|
||||||
|
|
||||||
|
$this->assertTrue($this->dispatcher->hasListeners(self::coreException));
|
||||||
|
$this->assertNumberListenersAdded(1, self::coreException);
|
||||||
|
$this->assertNumberListenersAdded(2);
|
||||||
|
|
||||||
|
$this->assertTrue($this->dispatcher->hasListeners(self::apiRequest));
|
||||||
|
$this->assertNumberListenersAdded(1, self::apiRequest);
|
||||||
|
$this->assertNumberListenersAdded(3);
|
||||||
|
|
||||||
|
$this->assertTrue($this->dispatcher->hasListeners(self::apiException));
|
||||||
|
$this->assertNumberListenersAdded(1, self::apiException);
|
||||||
|
$this->assertNumberListenersAdded(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDispatch()
|
||||||
|
{
|
||||||
|
$this->dispatcher->addListener('#', array($this->listener, 'onAny'));
|
||||||
|
$this->dispatcher->addListener('core.*', array($this->listener, 'onCore'));
|
||||||
|
$this->dispatcher->addListener('*.exception', array($this->listener, 'onException'));
|
||||||
|
$this->dispatcher->addListener(self::coreRequest, array($this->listener, 'onCoreRequest'));
|
||||||
|
|
||||||
|
$this->dispatcher->dispatch(self::coreRequest);
|
||||||
|
$this->dispatcher->dispatch(self::coreException);
|
||||||
|
$this->dispatcher->dispatch(self::apiRequest);
|
||||||
|
$this->dispatcher->dispatch(self::apiException);
|
||||||
|
|
||||||
|
$this->assertEquals(4, $this->listener->onAnyInvoked);
|
||||||
|
$this->assertEquals(2, $this->listener->onCoreInvoked);
|
||||||
|
$this->assertEquals(1, $this->listener->onCoreRequestInvoked);
|
||||||
|
$this->assertEquals(2, $this->listener->onExceptionInvoked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts the number of listeners added for a specific event or all events
|
||||||
|
* in total.
|
||||||
|
*
|
||||||
|
* @param integer $expected
|
||||||
|
* @param string $eventName
|
||||||
|
*/
|
||||||
|
private function assertNumberListenersAdded($expected, $eventName = null)
|
||||||
|
{
|
||||||
|
return isset($eventName)
|
||||||
|
? $this->assertEquals($expected, count($this->dispatcher->getListeners($eventName)))
|
||||||
|
: $this->assertEquals($expected, array_sum(array_map('count', $this->dispatcher->getListeners())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestEventListener
|
||||||
|
{
|
||||||
|
public $onAnyInvoked = 0;
|
||||||
|
public $onCoreInvoked = 0;
|
||||||
|
public $onCoreRequestInvoked = 0;
|
||||||
|
public $onExceptionInvoked = 0;
|
||||||
|
|
||||||
|
public function onAny(Event $e)
|
||||||
|
{
|
||||||
|
++$this->onAnyInvoked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onCore(Event $e)
|
||||||
|
{
|
||||||
|
++$this->onCoreInvoked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onCoreRequest(Event $e)
|
||||||
|
{
|
||||||
|
++$this->onCoreRequestInvoked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onException(Event $e)
|
||||||
|
{
|
||||||
|
++$this->onExceptionInvoked;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Jmikola\Tests\WildcardEventDispatcher;
|
||||||
|
|
||||||
|
use Jmikola\WildcardEventDispatcher\WildcardEventDispatcher;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
|
class EventDispatcherTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
private $dispatcher;
|
||||||
|
private $innerDispatcher;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->innerDispatcher = $this->getMockEventDispatcher();
|
||||||
|
$this->dispatcher = new WildcardEventDispatcher($this->innerDispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideListenersWithoutWildcards
|
||||||
|
*/
|
||||||
|
public function testShouldAddListenersWithoutWildcardsEagerly($eventName, $listener, $priority)
|
||||||
|
{
|
||||||
|
$this->innerDispatcher->expects($this->once())
|
||||||
|
->method('addListener')
|
||||||
|
->with($eventName, $listener, $priority);
|
||||||
|
|
||||||
|
$this->dispatcher->addListener($eventName, $listener, $priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideListenersWithoutWildcards()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
array('core.request', 'callback', 0),
|
||||||
|
array('core.exception', array('class', 'method'), 5),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideListenersWithWildcards
|
||||||
|
*/
|
||||||
|
public function testShouldAddListenersWithWildcardsLazily($eventName, $listener, $priority)
|
||||||
|
{
|
||||||
|
$this->innerDispatcher->expects($this->never())
|
||||||
|
->method('addListener');
|
||||||
|
|
||||||
|
$this->dispatcher->addListener($eventName, $listener, $priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideListenersWithWildcards()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
array('core.*', 'callback', 0),
|
||||||
|
array('#', array('class', 'method'), -10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldAddListenersWithWildcardsWhenMatchingEventIsDispatched()
|
||||||
|
{
|
||||||
|
$this->innerDispatcher->expects($this->once())
|
||||||
|
->id('listener-is-added')
|
||||||
|
->method('addListener')
|
||||||
|
->with('core.request', 'callback', 0);
|
||||||
|
|
||||||
|
$this->innerDispatcher->expects($this->once())
|
||||||
|
->after('listener-is-added')
|
||||||
|
->method('dispatch')
|
||||||
|
->with('core.request');
|
||||||
|
|
||||||
|
$this->dispatcher->addListener('core.*', 'callback', 0);
|
||||||
|
$this->dispatcher->dispatch('core.request');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldAddListenersWithWildcardsWhenListenersForMatchingEventsAreRetrieved()
|
||||||
|
{
|
||||||
|
$this->innerDispatcher->expects($this->once())
|
||||||
|
->id('listener-is-added')
|
||||||
|
->method('addListener')
|
||||||
|
->with('core.request', 'callback', 0);
|
||||||
|
|
||||||
|
$this->innerDispatcher->expects($this->once())
|
||||||
|
->after('listener-is-added')
|
||||||
|
->method('getListeners')
|
||||||
|
->with('core.request')
|
||||||
|
->will($this->returnValue(array('callback')));
|
||||||
|
|
||||||
|
$this->dispatcher->addListener('core.*', 'callback', 0);
|
||||||
|
|
||||||
|
$this->assertEquals(array('callback'), $this->dispatcher->getListeners('core.request'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldNotCountWildcardListenersThatHaveNeverBeenMatchedWhenAllListenersAreRetrieved()
|
||||||
|
{
|
||||||
|
/* When getListeners() is called without an event name, it attempts to
|
||||||
|
* return the collection of listeners for all events it knows about.
|
||||||
|
* When working with wildcards, we cannot anticipate events until we
|
||||||
|
* encounter a matching name. Therefore, getListeners() will ignore any
|
||||||
|
* wildcard listeners that are registered but haven't matched anything.
|
||||||
|
*/
|
||||||
|
$this->innerDispatcher->expects($this->never())
|
||||||
|
->method('addListener');
|
||||||
|
|
||||||
|
$this->innerDispatcher->expects($this->any())
|
||||||
|
->method('getListeners')
|
||||||
|
->will($this->returnValue(array()));
|
||||||
|
|
||||||
|
$this->dispatcher->addListener('core.*', 'callback', 0);
|
||||||
|
$this->assertEquals(array(), $this->dispatcher->getListeners());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddingAndRemovingAnEventSubscriber()
|
||||||
|
{
|
||||||
|
/* Since the EventSubscriberInterface defines getSubscribedEvents() as
|
||||||
|
* static, we cannot mock it with PHPUnit and must use a stub class.
|
||||||
|
*/
|
||||||
|
$subscriber = new TestEventSubscriber();
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
$defaultPriority = 0;
|
||||||
|
$numSubscribedEvents = count($subscriber->getSubscribedEvents());
|
||||||
|
|
||||||
|
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
|
||||||
|
$method = is_array($params) ? $params[0] : $params;
|
||||||
|
$priority = is_array($params) ? $params[1] : $defaultPriority;
|
||||||
|
|
||||||
|
$this->innerDispatcher->expects($this->at($i))
|
||||||
|
->method('addListener')
|
||||||
|
->with($eventName, array($subscriber, $method), $priority);
|
||||||
|
|
||||||
|
$this->innerDispatcher->expects($this->at($numSubscribedEvents + $i))
|
||||||
|
->method('removeListener')
|
||||||
|
->with($eventName, array($subscriber, $method));
|
||||||
|
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dispatcher->addSubscriber($subscriber, $priority);
|
||||||
|
$this->dispatcher->removeSubscriber($subscriber, $priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMockEventDispatcher()
|
||||||
|
{
|
||||||
|
return $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestEventSubscriber implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'core.request' => 'onRequest',
|
||||||
|
'core.exception' => array('onException', 10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (file_exists($file = __DIR__.'/../vendor/.composer/autoload.php')) {
|
||||||
|
$loader = require_once $file;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException('Install dependencies to run test suite.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$loader->add('Jmikola\\WildcardEventDispatcher', __DIR__.'/../src');
|
||||||
Reference in New Issue
Block a user