diff --git a/src/EventManager.php b/src/EventManager.php index b41df06..9d7c1d1 100644 --- a/src/EventManager.php +++ b/src/EventManager.php @@ -22,6 +22,22 @@ class EventManager implements EventManagerInterface /** * Subscribed events and their listeners * + * STRUCTURE: + * [ + * => [ + * => [ + * 0 => [, ...] + * ], + * ... + * ], + * ... + * ] + * + * NOTE: + * This structure helps us to reuse the list of listeners + * instead of first iterating over it and generating a new one + * -> In result it improves performance by up to 25% even if it looks a bit strange + * * @var array[] */ protected $events = []; @@ -116,8 +132,14 @@ public function trigger($eventName, $target = null, $argv = []) { $event = clone $this->eventPrototype; $event->setName($eventName); - $event->setTarget($target); - $event->setParams($argv); + + if ($target !== null) { + $event->setTarget($target); + } + + if ($argv) { + $event->setParams($argv); + } return $this->triggerListeners($event); } @@ -129,8 +151,14 @@ public function triggerUntil(callable $callback, $eventName, $target = null, $ar { $event = clone $this->eventPrototype; $event->setName($eventName); - $event->setTarget($target); - $event->setParams($argv); + + if ($target !== null) { + $event->setTarget($target); + } + + if ($argv) { + $event->setParams($argv); + } return $this->triggerListeners($event, $callback); } @@ -164,8 +192,7 @@ public function attach($eventName, callable $listener, $priority = 1) )); } - $this->events[$eventName][((int) $priority) . '.0'][] = $listener; - + $this->events[$eventName][(int) $priority][0][] = $listener; return $listener; } @@ -197,36 +224,35 @@ public function detach(callable $listener, $eventName = null, $force = false) } foreach ($this->events[$eventName] as $priority => $listeners) { - foreach ($listeners as $index => $evaluatedListener) { + foreach ($listeners[0] as $index => $evaluatedListener) { if ($evaluatedListener !== $listener) { continue; } // Found the listener; remove it. - unset($this->events[$eventName][$priority][$index]); + unset($this->events[$eventName][$priority][0][$index]); // If the queue for the given priority is empty, remove it. - if (empty($this->events[$eventName][$priority])) { + if (empty($this->events[$eventName][$priority][0])) { unset($this->events[$eventName][$priority]); break; } } + } - // If the queue for the given event is empty, remove it. - if (empty($this->events[$eventName])) { - unset($this->events[$eventName]); - break; - } + // If the queue for the given event is empty, remove it. + if (empty($this->events[$eventName])) { + unset($this->events[$eventName]); } } /** * @inheritDoc */ - public function clearListeners($event) + public function clearListeners($eventName) { - if (isset($this->events[$event])) { - unset($this->events[$event]); + if (isset($this->events[$eventName])) { + unset($this->events[$eventName]); } } @@ -262,58 +288,56 @@ protected function triggerListeners(EventInterface $event, callable $callback = throw new Exception\RuntimeException('Event is missing a name; cannot trigger!'); } - // Initial value of stop propagation flag should be false - $event->stopPropagation(false); - - $responses = new ResponseCollection(); + if (isset($this->events[$name])) { + $listOfListenersByPriority = $this->events[$name]; - foreach ($this->getListenersByEventName($name) as $listener) { - $response = $listener($event); - $responses->push($response); - - // If the event was asked to stop propagating, do so - if ($event->propagationIsStopped()) { - $responses->setStopped(true); - break; + if (isset($this->events['*'])) { + foreach ($this->events['*'] as $priority => $listOfListeners) { + $listOfListenersByPriority[$priority][] = $listOfListeners[0]; + } } + } elseif (isset($this->events['*'])) { + $listOfListenersByPriority = $this->events['*']; + } else { + $listOfListenersByPriority = []; + } - // If the result causes our validation callback to return true, - // stop propagation - if ($callback && $callback($response)) { - $responses->setStopped(true); - break; + if ($this->sharedManager) { + foreach ($this->sharedManager->getListeners($this->identifiers, $name) as $priority => $listeners) { + $listOfListenersByPriority[$priority][] = $listeners; } } - return $responses; - } - - /** - * Get listeners for the currently triggered event. - * - * @param string $eventName - * @return callable[] - */ - private function getListenersByEventName($eventName) - { - $listeners = array_merge_recursive( - isset($this->events[$eventName]) ? $this->events[$eventName] : [], - isset($this->events['*']) ? $this->events['*'] : [], - $this->sharedManager ? $this->sharedManager->getListeners($this->identifiers, $eventName) : [] - ); - - krsort($listeners, SORT_NUMERIC); + // Sort by priority in reverse order + krsort($listOfListenersByPriority); - $listenersForEvent = []; + // Initial value of stop propagation flag should be false + $event->stopPropagation(false); - foreach ($listeners as $priority => $listenersByPriority) { - foreach ($listenersByPriority as $listener) { - // Performance note: after some testing, it appears that accumulating listeners and sending - // them at the end of the method is FASTER than using generators (ie. yielding) - $listenersForEvent[] = $listener; + // Execute listeners + $responses = new ResponseCollection(); + foreach ($listOfListenersByPriority as $listOfListeners) { + foreach ($listOfListeners as $listeners) { + foreach ($listeners as $listener) { + $response = $listener($event); + $responses->push($response); + + // If the event was asked to stop propagating, do so + if ($event->propagationIsStopped()) { + $responses->setStopped(true); + return $responses; + } + + // If the result causes our validation callback to return true, + // stop propagation + if ($callback && $callback($response)) { + $responses->setStopped(true); + return $responses; + } + } } } - return $listenersForEvent; + return $responses; } } diff --git a/src/SharedEventManager.php b/src/SharedEventManager.php index 750fee3..b61701b 100644 --- a/src/SharedEventManager.php +++ b/src/SharedEventManager.php @@ -74,7 +74,7 @@ public function attach($identifier, $event, callable $listener, $priority = 1) )); } - $this->identifiers[$identifier][$event][((int) $priority) . '.0'][] = $listener; + $this->identifiers[$identifier][$event][(int) $priority][] = $listener; } /** @@ -141,7 +141,6 @@ public function detach(callable $listener, $identifier = null, $eventName = null unset($this->identifiers[$identifier][$eventName]); break; } - } // Is the identifier queue now empty? Remove it. @@ -153,8 +152,8 @@ public function detach(callable $listener, $identifier = null, $eventName = null /** * Retrieve all listeners for a given identifier and event * - * @param array $identifiers - * @param string $eventName + * @param string[] $identifiers + * @param string $eventName * @return array[] * @throws Exception\InvalidArgumentException */ @@ -167,7 +166,7 @@ public function getListeners(array $identifiers, $eventName) )); } - $listeners = []; + $returnListeners = []; foreach ($identifiers as $identifier) { if ('*' === $identifier || ! is_string($identifier) || empty($identifier)) { @@ -177,26 +176,42 @@ public function getListeners(array $identifiers, $eventName) )); } - $listenersByIdentifier = isset($this->identifiers[$identifier]) ? $this->identifiers[$identifier] : []; - - $listeners = array_merge_recursive( - $listeners, - isset($listenersByIdentifier[$eventName]) ? $listenersByIdentifier[$eventName] : [], - isset($listenersByIdentifier['*']) ? $listenersByIdentifier['*'] : [] - ); + if (isset($this->identifiers[$identifier])) { + $listenersByIdentifier = $this->identifiers[$identifier]; + if (isset($listenersByIdentifier[$eventName])) { + foreach ($listenersByIdentifier[$eventName] as $priority => $listeners) { + $returnListeners[$priority][] = $listeners; + } + } + if (isset($listenersByIdentifier['*'])) { + foreach ($listenersByIdentifier['*'] as $priority => $listeners) { + $returnListeners[$priority][] = $listeners; + } + } + } } - if (isset($this->identifiers['*']) && ! in_array('*', $identifiers)) { + if (isset($this->identifiers['*'])) { $wildcardIdentifier = $this->identifiers['*']; + if (isset($wildcardIdentifier[$eventName])) { + foreach ($wildcardIdentifier[$eventName] as $priority => $listeners) { + $returnListeners[$priority][] = $listeners; + } + } + if (isset($wildcardIdentifier['*'])) { + foreach ($wildcardIdentifier['*'] as $priority => $listeners) { + $returnListeners[$priority][] = $listeners; + } + } + } - $listeners = array_merge_recursive( - $listeners, - isset($wildcardIdentifier[$eventName]) ? $wildcardIdentifier[$eventName] : [], - isset($wildcardIdentifier['*']) ? $wildcardIdentifier['*'] : [] - ); + foreach ($returnListeners as $priority => $listOfListeners) { + // Argument unpacking requires PHP-5.6 + // $listeners[$priority] = array_merge(...$listOfListeners); + $returnListeners[$priority] = call_user_func_array('array_merge', $listOfListeners); } - return $listeners; + return $returnListeners; } /** @@ -204,7 +219,7 @@ public function getListeners(array $identifiers, $eventName) */ public function clearListeners($identifier, $eventName = null) { - if (! array_key_exists($identifier, $this->identifiers)) { + if (! isset($this->identifiers[$identifier])) { return false; } diff --git a/src/Test/EventListenerIntrospectionTrait.php b/src/Test/EventListenerIntrospectionTrait.php index 4f66766..35f3739 100644 --- a/src/Test/EventListenerIntrospectionTrait.php +++ b/src/Test/EventListenerIntrospectionTrait.php @@ -66,13 +66,16 @@ private function getListenersForEvent($event, EventManager $events, $withPriorit { $r = new ReflectionProperty($events, 'events'); $r->setAccessible(true); - $listeners = $r->getValue($events); + $internal = $r->getValue($events); - if (! isset($listeners[$event])) { - return $this->traverseListeners([]); + $listeners = []; + foreach (isset($internal[$event]) ? $internal[$event] : [] as $p => $listOfListeners) { + foreach ($listOfListeners as $l) { + $listeners[$p] = isset($listeners[$p]) ? array_merge($listeners[$p], $l) : $l; + } } - return $this->traverseListeners($listeners[$event], $withPriority); + return $this->traverseListeners($listeners, $withPriority); } /** diff --git a/test/EventManagerTest.php b/test/EventManagerTest.php index eb50cc7..a422211 100644 --- a/test/EventManagerTest.php +++ b/test/EventManagerTest.php @@ -56,7 +56,12 @@ public function getListenersForEvent($event, EventManager $manager) $r->setAccessible(true); $events = $r->getValue($manager); - return isset($events[$event]) ? $events[$event] : []; + $listenersByPriority = isset($events[$event]) ? $events[$event] : []; + foreach ($listenersByPriority as $priority => & $listeners) { + $listeners = $listeners[0]; + } + + return $listenersByPriority; } public function testAttachShouldAddListenerToEvent() diff --git a/test/SharedEventManagerTest.php b/test/SharedEventManagerTest.php index 5d7e95a..cd3c1c7 100644 --- a/test/SharedEventManagerTest.php +++ b/test/SharedEventManagerTest.php @@ -27,7 +27,7 @@ public function setUp() public function getListeners(SharedEventManager $manager, array $identifiers, $event, $priority = 1) { - $priority = (int) $priority . '.0'; + $priority = (int) $priority; $listeners = $manager->getListeners($identifiers, $event); if (! isset($listeners[$priority])) { return []; @@ -247,7 +247,7 @@ public function testClearListenersDoesNothingIfNoEventsRegisteredForIdentifier() $this->manager->clearListeners('IDENTIFIER', 'EVENT'); // getListeners() always pulls in wildcard listeners - $this->assertEquals(['1.0' => [ + $this->assertEquals([1 => [ $this->callback, ]], $this->manager->getListeners([ 'IDENTIFIER' ], 'EVENT')); } diff --git a/test/TestAsset/StaticEventsMock.php b/test/TestAsset/StaticEventsMock.php index 431acc6..dcb9cb9 100644 --- a/test/TestAsset/StaticEventsMock.php +++ b/test/TestAsset/StaticEventsMock.php @@ -30,7 +30,6 @@ public function getListeners($id, $event = null) */ public function attach($identifier, $event, callable $listener, $priority = 1) { - } /**