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