diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index ce5d7726279a..f3b76d52c6ab 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -18,6 +18,11 @@ UPGRADE FROM 2.x to 3.0 `DebugClassLoader`. The difference is that the constructor now takes a loader to wrap. +### EventDispatcher + + * The interface `Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface` + extends `Symfony\Component\EventDispatcher\EventDispatcherInterface`. + ### Form * The methods `Form::bind()` and `Form::isBound()` were removed. You should diff --git a/src/Symfony/Component/EventDispatcher/CHANGELOG.md b/src/Symfony/Component/EventDispatcher/CHANGELOG.md index 067c6c233f14..bb42ee19c04c 100644 --- a/src/Symfony/Component/EventDispatcher/CHANGELOG.md +++ b/src/Symfony/Component/EventDispatcher/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 2.5.0 ----- + * added Debug\TraceableEventDispatcher (originally in HttpKernel) + * changed Debug\TraceableEventDispatcherInterface to extend EventDispatcherInterface * added RegisterListenersPass (originally in HttpKernel) 2.1.0 diff --git a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php new file mode 100644 index 000000000000..fc7bcb4dd601 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php @@ -0,0 +1,369 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Debug; + +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\Stopwatch\Stopwatch; +use Psr\Log\LoggerInterface; + +/** + * Collects some data about event listeners. + * + * This event dispatcher delegates the dispatching to another one. + * + * @author Fabien Potencier + */ +class TraceableEventDispatcher implements TraceableEventDispatcherInterface +{ + protected $logger; + protected $stopwatch; + private $called = array(); + private $dispatcher; + private $wrappedListeners = array(); + private $firstCalledEvent = array(); + private $id; + private $lastEventId = 0; + + /** + * Constructor. + * + * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance + * @param Stopwatch $stopwatch A Stopwatch instance + * @param LoggerInterface $logger A LoggerInterface instance + */ + public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null) + { + $this->dispatcher = $dispatcher; + $this->stopwatch = $stopwatch; + $this->logger = $logger; + } + + /** + * {@inheritDoc} + */ + public function addListener($eventName, $listener, $priority = 0) + { + $this->dispatcher->addListener($eventName, $listener, $priority); + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + $this->dispatcher->addSubscriber($subscriber); + } + + /** + * {@inheritdoc} + */ + public function removeListener($eventName, $listener) + { + return $this->dispatcher->removeListener($eventName, $listener); + } + + /** + * {@inheritdoc} + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + return $this->dispatcher->removeSubscriber($subscriber); + } + + /** + * {@inheritdoc} + */ + public function getListeners($eventName = null) + { + return $this->dispatcher->getListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function hasListeners($eventName = null) + { + return $this->dispatcher->hasListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function dispatch($eventName, Event $event = null) + { + if (null === $event) { + $event = new Event(); + } + + $this->id = $eventId = ++$this->lastEventId; + + // Wrap all listeners before they are called + $this->wrappedListeners[$this->id] = new \SplObjectStorage(); + + $listeners = $this->dispatcher->getListeners($eventName); + + foreach ($listeners as $listener) { + $this->dispatcher->removeListener($eventName, $listener); + $wrapped = $this->wrapListener($eventName, $listener); + $this->wrappedListeners[$this->id][$wrapped] = $listener; + $this->dispatcher->addListener($eventName, $wrapped); + } + + $this->preDispatch($eventName, $event); + + $e = $this->stopwatch->start($eventName, 'section'); + + $this->firstCalledEvent[$eventName] = $this->stopwatch->start($eventName.'.loading', 'event_listener_loading'); + + if (!$this->dispatcher->hasListeners($eventName)) { + $this->firstCalledEvent[$eventName]->stop(); + } + + $this->dispatcher->dispatch($eventName, $event); + + // reset the id as another event might have been dispatched during the dispatching of this event + $this->id = $eventId; + + unset($this->firstCalledEvent[$eventName]); + + if ($e->isStarted()) { + $e->stop(); + } + + $this->postDispatch($eventName, $event); + + // Unwrap all listeners after they are called + foreach ($this->wrappedListeners[$this->id] as $wrapped) { + $this->dispatcher->removeListener($eventName, $wrapped); + $this->dispatcher->addListener($eventName, $this->wrappedListeners[$this->id][$wrapped]); + } + + unset($this->wrappedListeners[$this->id]); + + return $event; + } + + /** + * {@inheritDoc} + */ + public function getCalledListeners() + { + return $this->called; + } + + /** + * {@inheritDoc} + */ + public function getNotCalledListeners() + { + $notCalled = array(); + + foreach ($this->getListeners() as $name => $listeners) { + foreach ($listeners as $listener) { + $info = $this->getListenerInfo($listener, $name); + if (!isset($this->called[$name.'.'.$info['pretty']])) { + $notCalled[$name.'.'.$info['pretty']] = $info; + } + } + } + + return $notCalled; + } + + /** + * Proxies all method calls to the original event dispatcher. + * + * @param string $method The method name + * @param array $arguments The method arguments + * + * @return mixed + */ + public function __call($method, $arguments) + { + return call_user_func_array(array($this->dispatcher, $method), $arguments); + } + + /** + * This is a private method and must not be used. + * + * This method is public because it is used in a closure. + * Whenever Symfony will require PHP 5.4, this could be changed + * to a proper private method. + */ + public function logSkippedListeners($eventName, Event $event, $listener) + { + if (null === $this->logger) { + return; + } + + $info = $this->getListenerInfo($listener, $eventName); + + $this->logger->debug(sprintf('Listener "%s" stopped propagation of the event "%s".', $info['pretty'], $eventName)); + + $skippedListeners = $this->getListeners($eventName); + $skipped = false; + + foreach ($skippedListeners as $skippedListener) { + $skippedListener = $this->unwrapListener($skippedListener); + + if ($skipped) { + $info = $this->getListenerInfo($skippedListener, $eventName); + $this->logger->debug(sprintf('Listener "%s" was not called for event "%s".', $info['pretty'], $eventName)); + } + + if ($skippedListener === $listener) { + $skipped = true; + } + } + } + + /** + * This is a private method. + * + * This method is public because it is used in a closure. + * Whenever Symfony will require PHP 5.4, this could be changed + * to a proper private method. + */ + public function preListenerCall($eventName, $listener) + { + // is it the first called listener? + if (isset($this->firstCalledEvent[$eventName])) { + $this->firstCalledEvent[$eventName]->stop(); + + unset($this->firstCalledEvent[$eventName]); + } + + $info = $this->getListenerInfo($listener, $eventName); + + if (null !== $this->logger) { + $this->logger->debug(sprintf('Notified event "%s" to listener "%s".', $eventName, $info['pretty'])); + } + + $this->called[$eventName.'.'.$info['pretty']] = $info; + + return $this->stopwatch->start(isset($info['class']) ? $info['class'] : $info['type'], 'event_listener'); + } + + /** + * Returns information about the listener + * + * @param object $listener The listener + * @param string $eventName The event name + * + * @return array Informations about the listener + */ + private function getListenerInfo($listener, $eventName) + { + $listener = $this->unwrapListener($listener); + + $info = array( + 'event' => $eventName, + ); + if ($listener instanceof \Closure) { + $info += array( + 'type' => 'Closure', + 'pretty' => 'closure' + ); + } elseif (is_string($listener)) { + try { + $r = new \ReflectionFunction($listener); + $file = $r->getFileName(); + $line = $r->getStartLine(); + } catch (\ReflectionException $e) { + $file = null; + $line = null; + } + $info += array( + 'type' => 'Function', + 'function' => $listener, + 'file' => $file, + 'line' => $line, + 'pretty' => $listener, + ); + } elseif (is_array($listener) || (is_object($listener) && is_callable($listener))) { + if (!is_array($listener)) { + $listener = array($listener, '__invoke'); + } + $class = is_object($listener[0]) ? get_class($listener[0]) : $listener[0]; + try { + $r = new \ReflectionMethod($class, $listener[1]); + $file = $r->getFileName(); + $line = $r->getStartLine(); + } catch (\ReflectionException $e) { + $file = null; + $line = null; + } + $info += array( + 'type' => 'Method', + 'class' => $class, + 'method' => $listener[1], + 'file' => $file, + 'line' => $line, + 'pretty' => $class.'::'.$listener[1], + ); + } + + return $info; + } + + /** + * Called before dispatching the event. + * + * @param string $eventName The event name + * @param Event $event The event + */ + protected function preDispatch($eventName, Event $event) + { + } + + /** + * Called after dispatching the event. + * + * @param string $eventName The event name + * @param Event $event The event + */ + protected function postDispatch($eventName, Event $event) + { + } + + private function wrapListener($eventName, $listener) + { + $self = $this; + + return function (Event $event) use ($self, $eventName, $listener) { + $e = $self->preListenerCall($eventName, $listener); + + call_user_func($listener, $event, $eventName, $self); + + if ($e->isStarted()) { + $e->stop(); + } + + if ($event->isPropagationStopped()) { + $self->logSkippedListeners($eventName, $event, $listener); + } + }; + } + + private function unwrapListener($listener) + { + // get the original listener + if (is_object($listener) && isset($this->wrappedListeners[$this->id][$listener])) { + return $this->wrappedListeners[$this->id][$listener]; + } + + return $listener; + } +} diff --git a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php index a67a979014f9..5483e815068c 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php +++ b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php @@ -11,10 +11,12 @@ namespace Symfony\Component\EventDispatcher\Debug; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + /** * @author Fabien Potencier */ -interface TraceableEventDispatcherInterface +interface TraceableEventDispatcherInterface extends EventDispatcherInterface { /** * Gets the called listeners. diff --git a/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php new file mode 100644 index 000000000000..8ccfabb1ca40 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php @@ -0,0 +1,171 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Tests\Debug; + +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\Stopwatch\Stopwatch; + +class TraceableEventDispatcherTest extends \PHPUnit_Framework_TestCase +{ + public function testAddRemoveListener() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + + $tdispatcher->addListener('foo', $listener = function () { ; }); + $listeners = $dispatcher->getListeners('foo'); + $this->assertCount(1, $listeners); + $this->assertSame($listener, $listeners[0]); + + $tdispatcher->removeListener('foo', $listener); + $this->assertCount(0, $dispatcher->getListeners('foo')); + } + + public function testGetListeners() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + + $tdispatcher->addListener('foo', $listener = function () { ; }); + $this->assertSame($dispatcher->getListeners('foo'), $tdispatcher->getListeners('foo')); + } + + public function testHasListeners() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + + $this->assertFalse($dispatcher->hasListeners('foo')); + $this->assertFalse($tdispatcher->hasListeners('foo')); + + $tdispatcher->addListener('foo', $listener = function () { ; }); + $this->assertTrue($dispatcher->hasListeners('foo')); + $this->assertTrue($tdispatcher->hasListeners('foo')); + } + + public function testAddRemoveSubscriber() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + + $subscriber = new EventSubscriber(); + + $tdispatcher->addSubscriber($subscriber); + $listeners = $dispatcher->getListeners('foo'); + $this->assertCount(1, $listeners); + $this->assertSame(array($subscriber, 'call'), $listeners[0]); + + $tdispatcher->removeSubscriber($subscriber); + $this->assertCount(0, $dispatcher->getListeners('foo')); + } + + public function testGetCalledListeners() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + $tdispatcher->addListener('foo', $listener = function () { ; }); + + $this->assertEquals(array(), $tdispatcher->getCalledListeners()); + $this->assertEquals(array('foo.closure' => array('event' => 'foo', 'type' => 'Closure', 'pretty' => 'closure')), $tdispatcher->getNotCalledListeners()); + + $tdispatcher->dispatch('foo'); + + $this->assertEquals(array('foo.closure' => array('event' => 'foo', 'type' => 'Closure', 'pretty' => 'closure')), $tdispatcher->getCalledListeners()); + $this->assertEquals(array(), $tdispatcher->getNotCalledListeners()); + } + + public function testLogger() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); + $tdispatcher->addListener('foo', $listener1 = function () { ; }); + $tdispatcher->addListener('foo', $listener2 = function () { ; }); + + $logger->expects($this->at(0))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); + $logger->expects($this->at(1))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); + + $tdispatcher->dispatch('foo'); + } + + public function testLoggerWithStoppedEvent() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); + $tdispatcher->addListener('foo', $listener1 = function (Event $event) { $event->stopPropagation(); }); + $tdispatcher->addListener('foo', $listener2 = function () { ; }); + + $logger->expects($this->at(0))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); + $logger->expects($this->at(1))->method('debug')->with("Listener \"closure\" stopped propagation of the event \"foo\"."); + $logger->expects($this->at(2))->method('debug')->with("Listener \"closure\" was not called for event \"foo\"."); + + $tdispatcher->dispatch('foo'); + } + + public function testDispatchCallListeners() + { + $called = array(); + + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + $tdispatcher->addListener('foo', $listener1 = function () use (&$called) { $called[] = 'foo1'; }); + $tdispatcher->addListener('foo', $listener2 = function () use (&$called) { $called[] = 'foo2'; }); + + $tdispatcher->dispatch('foo'); + + $this->assertEquals(array('foo1', 'foo2'), $called); + } + + public function testDispatchNested() + { + $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + $loop = 1; + $dispatcher->addListener('foo', $listener1 = function () use ($dispatcher, &$loop) { + ++$loop; + if (2 == $loop) { + $dispatcher->dispatch('foo'); + } + }); + + $dispatcher->dispatch('foo'); + } + + public function testDispatchReusedEventNested() + { + $nestedCall = false; + $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + $dispatcher->addListener('foo', function (Event $e) use ($dispatcher) { + $dispatcher->dispatch('bar', $e); + }); + $dispatcher->addListener('bar', function (Event $e) use (&$nestedCall) { + $nestedCall = true; + }); + + $this->assertFalse($nestedCall); + $dispatcher->dispatch('foo'); + $this->assertTrue($nestedCall); + } +} + +class EventSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array('foo' => 'call'); + } +} diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json index 054b8131c1d9..6343b5d1d9c7 100644 --- a/src/Symfony/Component/EventDispatcher/composer.json +++ b/src/Symfony/Component/EventDispatcher/composer.json @@ -19,7 +19,9 @@ "php": ">=5.3.3" }, "require-dev": { - "symfony/dependency-injection": "~2.0" + "symfony/dependency-injection": "~2.0", + "symfony/stopwatch": "~2.2", + "psr/log": "~1.0" }, "suggest": { "symfony/dependency-injection": "", diff --git a/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php index 4c28abef6164..ed2417be721b 100644 --- a/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php @@ -11,14 +11,10 @@ namespace Symfony\Component\HttpKernel\Debug; -use Symfony\Component\Stopwatch\Stopwatch; -use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher as BaseTraceableEventDispatcher; use Symfony\Component\HttpKernel\Profiler\Profiler; -use Psr\Log\LoggerInterface; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\EventDispatcher\Event; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface; /** * Collects some data about event listeners. @@ -27,31 +23,8 @@ * * @author Fabien Potencier */ -class TraceableEventDispatcher implements EventDispatcherInterface, TraceableEventDispatcherInterface +class TraceableEventDispatcher extends BaseTraceableEventDispatcher { - private $logger; - private $called = array(); - private $stopwatch; - private $dispatcher; - private $wrappedListeners = array(); - private $firstCalledEvent = array(); - private $id; - private $lastEventId = 0; - - /** - * Constructor. - * - * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance - * @param Stopwatch $stopwatch A Stopwatch instance - * @param LoggerInterface $logger A LoggerInterface instance - */ - public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null) - { - $this->dispatcher = $dispatcher; - $this->stopwatch = $stopwatch; - $this->logger = $logger; - } - /** * Sets the profiler. * @@ -67,268 +40,11 @@ public function setProfiler(Profiler $profiler = null) { } - /** - * {@inheritDoc} - */ - public function addListener($eventName, $listener, $priority = 0) - { - $this->dispatcher->addListener($eventName, $listener, $priority); - } - - /** - * {@inheritdoc} - */ - public function addSubscriber(EventSubscriberInterface $subscriber) - { - $this->dispatcher->addSubscriber($subscriber); - } - - /** - * {@inheritdoc} - */ - public function removeListener($eventName, $listener) - { - return $this->dispatcher->removeListener($eventName, $listener); - } - - /** - * {@inheritdoc} - */ - public function removeSubscriber(EventSubscriberInterface $subscriber) - { - return $this->dispatcher->removeSubscriber($subscriber); - } - /** * {@inheritdoc} */ - public function getListeners($eventName = null) + protected function preDispatch($eventName, Event $event) { - return $this->dispatcher->getListeners($eventName); - } - - /** - * {@inheritdoc} - */ - public function hasListeners($eventName = null) - { - return $this->dispatcher->hasListeners($eventName); - } - - /** - * {@inheritdoc} - */ - public function dispatch($eventName, Event $event = null) - { - if (null === $event) { - $event = new Event(); - } - - $this->id = $eventId = ++$this->lastEventId; - - $this->preDispatch($eventName, $event); - - $e = $this->stopwatch->start($eventName, 'section'); - - $this->firstCalledEvent[$eventName] = $this->stopwatch->start($eventName.'.loading', 'event_listener_loading'); - - if (!$this->dispatcher->hasListeners($eventName)) { - $this->firstCalledEvent[$eventName]->stop(); - } - - $this->dispatcher->dispatch($eventName, $event); - - // reset the id as another event might have been dispatched during the dispatching of this event - $this->id = $eventId; - - unset($this->firstCalledEvent[$eventName]); - - if ($e->isStarted()) { - $e->stop(); - } - - $this->postDispatch($eventName, $event); - - return $event; - } - - /** - * {@inheritDoc} - */ - public function getCalledListeners() - { - return $this->called; - } - - /** - * {@inheritDoc} - */ - public function getNotCalledListeners() - { - $notCalled = array(); - - foreach ($this->getListeners() as $name => $listeners) { - foreach ($listeners as $listener) { - $info = $this->getListenerInfo($listener, $name); - if (!isset($this->called[$name.'.'.$info['pretty']])) { - $notCalled[$name.'.'.$info['pretty']] = $info; - } - } - } - - return $notCalled; - } - - /** - * Proxies all method calls to the original event dispatcher. - * - * @param string $method The method name - * @param array $arguments The method arguments - * - * @return mixed - */ - public function __call($method, $arguments) - { - return call_user_func_array(array($this->dispatcher, $method), $arguments); - } - - /** - * This is a private method and must not be used. - * - * This method is public because it is used in a closure. - * Whenever Symfony will require PHP 5.4, this could be changed - * to a proper private method. - */ - public function logSkippedListeners($eventName, Event $event, $listener) - { - if (null === $this->logger) { - return; - } - - $info = $this->getListenerInfo($listener, $eventName); - - $this->logger->debug(sprintf('Listener "%s" stopped propagation of the event "%s".', $info['pretty'], $eventName)); - - $skippedListeners = $this->getListeners($eventName); - $skipped = false; - - foreach ($skippedListeners as $skippedListener) { - $skippedListener = $this->unwrapListener($skippedListener); - - if ($skipped) { - $info = $this->getListenerInfo($skippedListener, $eventName); - $this->logger->debug(sprintf('Listener "%s" was not called for event "%s".', $info['pretty'], $eventName)); - } - - if ($skippedListener === $listener) { - $skipped = true; - } - } - } - - /** - * This is a private method. - * - * This method is public because it is used in a closure. - * Whenever Symfony will require PHP 5.4, this could be changed - * to a proper private method. - */ - public function preListenerCall($eventName, $listener) - { - // is it the first called listener? - if (isset($this->firstCalledEvent[$eventName])) { - $this->firstCalledEvent[$eventName]->stop(); - - unset($this->firstCalledEvent[$eventName]); - } - - $info = $this->getListenerInfo($listener, $eventName); - - if (null !== $this->logger) { - $this->logger->debug(sprintf('Notified event "%s" to listener "%s".', $eventName, $info['pretty'])); - } - - $this->called[$eventName.'.'.$info['pretty']] = $info; - - return $this->stopwatch->start(isset($info['class']) ? $info['class'] : $info['type'], 'event_listener'); - } - - /** - * Returns information about the listener - * - * @param object $listener The listener - * @param string $eventName The event name - * - * @return array Informations about the listener - */ - private function getListenerInfo($listener, $eventName) - { - $listener = $this->unwrapListener($listener); - - $info = array( - 'event' => $eventName, - ); - if ($listener instanceof \Closure) { - $info += array( - 'type' => 'Closure', - 'pretty' => 'closure' - ); - } elseif (is_string($listener)) { - try { - $r = new \ReflectionFunction($listener); - $file = $r->getFileName(); - $line = $r->getStartLine(); - } catch (\ReflectionException $e) { - $file = null; - $line = null; - } - $info += array( - 'type' => 'Function', - 'function' => $listener, - 'file' => $file, - 'line' => $line, - 'pretty' => $listener, - ); - } elseif (is_array($listener) || (is_object($listener) && is_callable($listener))) { - if (!is_array($listener)) { - $listener = array($listener, '__invoke'); - } - $class = is_object($listener[0]) ? get_class($listener[0]) : $listener[0]; - try { - $r = new \ReflectionMethod($class, $listener[1]); - $file = $r->getFileName(); - $line = $r->getStartLine(); - } catch (\ReflectionException $e) { - $file = null; - $line = null; - } - $info += array( - 'type' => 'Method', - 'class' => $class, - 'method' => $listener[1], - 'file' => $file, - 'line' => $line, - 'pretty' => $class.'::'.$listener[1], - ); - } - - return $info; - } - - private function preDispatch($eventName, Event $event) - { - // wrap all listeners before they are called - $this->wrappedListeners[$this->id] = new \SplObjectStorage(); - - $listeners = $this->dispatcher->getListeners($eventName); - - foreach ($listeners as $listener) { - $this->dispatcher->removeListener($eventName, $listener); - $wrapped = $this->wrapListener($eventName, $listener); - $this->wrappedListeners[$this->id][$wrapped] = $listener; - $this->dispatcher->addListener($eventName, $wrapped); - } - switch ($eventName) { case KernelEvents::REQUEST: $this->stopwatch->openSection(); @@ -342,7 +58,7 @@ private function preDispatch($eventName, Event $event) break; case KernelEvents::TERMINATE: $token = $event->getResponse()->headers->get('X-Debug-Token'); - // There is a very special case when using builtin AppCache class as kernel wrapper, in the case + // There is a very special case when using built-in AppCache class as kernel wrapper, in the case // of an ESI request leading to a `stale` response [B] inside a `fresh` cached response [A]. // In this case, `$token` contains the [B] debug token, but the open `stopwatch` section ID // is equal to the [A] debug token. Trying to reopen section with the [B] token throws an exception @@ -354,7 +70,10 @@ private function preDispatch($eventName, Event $event) } } - private function postDispatch($eventName, Event $event) + /** + * {@inheritdoc} + */ + protected function postDispatch($eventName, Event $event) { switch ($eventName) { case KernelEvents::CONTROLLER: @@ -373,41 +92,5 @@ private function postDispatch($eventName, Event $event) } catch (\LogicException $e) {} break; } - - foreach ($this->wrappedListeners[$this->id] as $wrapped) { - $this->dispatcher->removeListener($eventName, $wrapped); - $this->dispatcher->addListener($eventName, $this->wrappedListeners[$this->id][$wrapped]); - } - - unset($this->wrappedListeners[$this->id]); - } - - private function wrapListener($eventName, $listener) - { - $self = $this; - - return function (Event $event) use ($self, $eventName, $listener) { - $e = $self->preListenerCall($eventName, $listener); - - call_user_func($listener, $event, $eventName, $self); - - if ($e->isStarted()) { - $e->stop(); - } - - if ($event->isPropagationStopped()) { - $self->logSkippedListeners($eventName, $event, $listener); - } - }; - } - - private function unwrapListener($listener) - { - // get the original listener - if (is_object($listener) && isset($this->wrappedListeners[$this->id][$listener])) { - return $this->wrappedListeners[$this->id][$listener]; - } - - return $listener; } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php b/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php index d30837d2c685..0b4af59d59fa 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php @@ -12,8 +12,6 @@ namespace Symfony\Component\HttpKernel\Tests\Debug; use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\EventDispatcher\Event; use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpFoundation\Request; @@ -22,148 +20,6 @@ class TraceableEventDispatcherTest extends \PHPUnit_Framework_TestCase { - public function testAddRemoveListener() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - - $tdispatcher->addListener('foo', $listener = function () { ; }); - $listeners = $dispatcher->getListeners('foo'); - $this->assertCount(1, $listeners); - $this->assertSame($listener, $listeners[0]); - - $tdispatcher->removeListener('foo', $listener); - $this->assertCount(0, $dispatcher->getListeners('foo')); - } - - public function testGetListeners() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - - $tdispatcher->addListener('foo', $listener = function () { ; }); - $this->assertSame($dispatcher->getListeners('foo'), $tdispatcher->getListeners('foo')); - } - - public function testHasListeners() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - - $this->assertFalse($dispatcher->hasListeners('foo')); - $this->assertFalse($tdispatcher->hasListeners('foo')); - - $tdispatcher->addListener('foo', $listener = function () { ; }); - $this->assertTrue($dispatcher->hasListeners('foo')); - $this->assertTrue($tdispatcher->hasListeners('foo')); - } - - public function testAddRemoveSubscriber() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - - $subscriber = new EventSubscriber(); - - $tdispatcher->addSubscriber($subscriber); - $listeners = $dispatcher->getListeners('foo'); - $this->assertCount(1, $listeners); - $this->assertSame(array($subscriber, 'call'), $listeners[0]); - - $tdispatcher->removeSubscriber($subscriber); - $this->assertCount(0, $dispatcher->getListeners('foo')); - } - - public function testGetCalledListeners() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - $tdispatcher->addListener('foo', $listener = function () { ; }); - - $this->assertEquals(array(), $tdispatcher->getCalledListeners()); - $this->assertEquals(array('foo.closure' => array('event' => 'foo', 'type' => 'Closure', 'pretty' => 'closure')), $tdispatcher->getNotCalledListeners()); - - $tdispatcher->dispatch('foo'); - - $this->assertEquals(array('foo.closure' => array('event' => 'foo', 'type' => 'Closure', 'pretty' => 'closure')), $tdispatcher->getCalledListeners()); - $this->assertEquals(array(), $tdispatcher->getNotCalledListeners()); - } - - public function testLogger() - { - $logger = $this->getMock('Psr\Log\LoggerInterface'); - - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); - $tdispatcher->addListener('foo', $listener1 = function () { ; }); - $tdispatcher->addListener('foo', $listener2 = function () { ; }); - - $logger->expects($this->at(0))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); - $logger->expects($this->at(1))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); - - $tdispatcher->dispatch('foo'); - } - - public function testLoggerWithStoppedEvent() - { - $logger = $this->getMock('Psr\Log\LoggerInterface'); - - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); - $tdispatcher->addListener('foo', $listener1 = function (Event $event) { $event->stopPropagation(); }); - $tdispatcher->addListener('foo', $listener2 = function () { ; }); - - $logger->expects($this->at(0))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); - $logger->expects($this->at(1))->method('debug')->with("Listener \"closure\" stopped propagation of the event \"foo\"."); - $logger->expects($this->at(2))->method('debug')->with("Listener \"closure\" was not called for event \"foo\"."); - - $tdispatcher->dispatch('foo'); - } - - public function testDispatchCallListeners() - { - $called = array(); - - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - $tdispatcher->addListener('foo', $listener1 = function () use (&$called) { $called[] = 'foo1'; }); - $tdispatcher->addListener('foo', $listener2 = function () use (&$called) { $called[] = 'foo2'; }); - - $tdispatcher->dispatch('foo'); - - $this->assertEquals(array('foo1', 'foo2'), $called); - } - - public function testDispatchNested() - { - $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); - $loop = 1; - $dispatcher->addListener('foo', $listener1 = function () use ($dispatcher, &$loop) { - ++$loop; - if (2 == $loop) { - $dispatcher->dispatch('foo'); - } - }); - - $dispatcher->dispatch('foo'); - } - - public function testDispatchReusedEventNested() - { - $nestedCall = false; - $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); - $dispatcher->addListener('foo', function (Event $e) use ($dispatcher) { - $dispatcher->dispatch('bar', $e); - }); - $dispatcher->addListener('bar', function (Event $e) use (&$nestedCall) { - $nestedCall = true; - }); - - $this->assertFalse($nestedCall); - $dispatcher->dispatch('foo'); - $this->assertTrue($nestedCall); - } - public function testStopwatchSections() { $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch = new Stopwatch()); @@ -232,11 +88,3 @@ protected function getHttpKernel($dispatcher, $controller) return new HttpKernel($dispatcher, $resolver); } } - -class EventSubscriber implements EventSubscriberInterface -{ - public static function getSubscribedEvents() - { - return array('foo' => 'call'); - } -}