commit 635305a7adb460b2b718302ef3799bad5126cf23 Author: Jeremy Mikola Date: Mon Feb 13 01:00:04 2012 -0500 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92d4786 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +phpunit.xml +vendor +composer.lock +composer.phar + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..755ff7b --- /dev/null +++ b/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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5fa9987 --- /dev/null +++ b/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 +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 +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 +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 +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 +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 +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) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..95694f5 --- /dev/null +++ b/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" +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d18eeaa --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + + ./tests/ + + + + + + ./ + + ./tests + + + + diff --git a/src/Jmikola/WildcardEventDispatcher/LazyListenerPattern.php b/src/Jmikola/WildcardEventDispatcher/LazyListenerPattern.php new file mode 100644 index 0000000..ba4beea --- /dev/null +++ b/src/Jmikola/WildcardEventDispatcher/LazyListenerPattern.php @@ -0,0 +1,45 @@ +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; + } +} diff --git a/src/Jmikola/WildcardEventDispatcher/ListenerPattern.php b/src/Jmikola/WildcardEventDispatcher/ListenerPattern.php new file mode 100644 index 0000000..abd6017 --- /dev/null +++ b/src/Jmikola/WildcardEventDispatcher/ListenerPattern.php @@ -0,0 +1,119 @@ + '(?:\.\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, '/') + )); + } +} \ No newline at end of file diff --git a/src/Jmikola/WildcardEventDispatcher/WildcardEventDispatcher.php b/src/Jmikola/WildcardEventDispatcher/WildcardEventDispatcher.php new file mode 100644 index 0000000..d38a485 --- /dev/null +++ b/src/Jmikola/WildcardEventDispatcher/WildcardEventDispatcher.php @@ -0,0 +1,188 @@ +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]); + } + } + } +} diff --git a/tests/Jmikola/Tests/WildcardEventDispatcher/LazyListenerPatternTest.php b/tests/Jmikola/Tests/WildcardEventDispatcher/LazyListenerPatternTest.php new file mode 100644 index 0000000..2895f12 --- /dev/null +++ b/tests/Jmikola/Tests/WildcardEventDispatcher/LazyListenerPatternTest.php @@ -0,0 +1,34 @@ +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'); + } +} diff --git a/tests/Jmikola/Tests/WildcardEventDispatcher/ListenerPatternTest.php b/tests/Jmikola/Tests/WildcardEventDispatcher/ListenerPatternTest.php new file mode 100644 index 0000000..e922b51 --- /dev/null +++ b/tests/Jmikola/Tests/WildcardEventDispatcher/ListenerPatternTest.php @@ -0,0 +1,91 @@ +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'); + } +} diff --git a/tests/Jmikola/Tests/WildcardEventDispatcher/WildcardEventDispatcherFunctionalTest.php b/tests/Jmikola/Tests/WildcardEventDispatcher/WildcardEventDispatcherFunctionalTest.php new file mode 100644 index 0000000..af18b1a --- /dev/null +++ b/tests/Jmikola/Tests/WildcardEventDispatcher/WildcardEventDispatcherFunctionalTest.php @@ -0,0 +1,161 @@ +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; + } +} diff --git a/tests/Jmikola/Tests/WildcardEventDispatcher/WildcardEventDispatcherTest.php b/tests/Jmikola/Tests/WildcardEventDispatcher/WildcardEventDispatcherTest.php new file mode 100644 index 0000000..30d9b85 --- /dev/null +++ b/tests/Jmikola/Tests/WildcardEventDispatcher/WildcardEventDispatcherTest.php @@ -0,0 +1,156 @@ +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), + ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..e79f564 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,9 @@ +add('Jmikola\\WildcardEventDispatcher', __DIR__.'/../src');