Skip to content
Browse files

First commit

  • Loading branch information...
0 parents commit 635305a7adb460b2b718302ef3799bad5126cf23 @jmikola committed Feb 13, 2012
5 .gitignore
@@ -0,0 +1,5 @@
+phpunit.xml
+vendor
+composer.lock
+composer.phar
+
18 LICENSE
@@ -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.
138 README.md
@@ -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)
19 composer.json
@@ -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"
+}
18 phpunit.xml.dist
@@ -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>
45 src/Jmikola/WildcardEventDispatcher/LazyListenerPattern.php
@@ -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;
+ }
+}
119 src/Jmikola/WildcardEventDispatcher/ListenerPattern.php
@@ -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, '/')
+ ));
+ }
+}
188 src/Jmikola/WildcardEventDispatcher/WildcardEventDispatcher.php
@@ -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]);
+ }
+ }
+ }
+}
34 tests/Jmikola/Tests/WildcardEventDispatcher/LazyListenerPatternTest.php
@@ -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');
+ }
+}
91 tests/Jmikola/Tests/WildcardEventDispatcher/ListenerPatternTest.php
@@ -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 tests/Jmikola/Tests/WildcardEventDispatcher/WildcardEventDispatcherFunctionalTest.php
@@ -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;
+ }
+}
156 tests/Jmikola/Tests/WildcardEventDispatcher/WildcardEventDispatcherTest.php
@@ -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),
+ );
+ }
+}
9 tests/bootstrap.php
@@ -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');

0 comments on commit 635305a

Please sign in to comment.
Something went wrong with that request. Please try again.