From 9a90e0621ce00ac3833c1bf9018f7d8bc5a15a17 Mon Sep 17 00:00:00 2001 From: florianv Date: Sat, 28 Dec 2013 20:36:11 +0100 Subject: [PATCH] [EventDispatcher] Added TraceableEventDispatcher from HttpKernel --- UPGRADE-3.0.md | 5 + .../Component/EventDispatcher/CHANGELOG.md | 2 + .../Debug/TraceableEventDispatcher.php | 369 ++++++++++++++++++ .../TraceableEventDispatcherInterface.php | 4 +- .../Debug/TraceableEventDispatcherTest.php | 171 ++++++++ .../Component/EventDispatcher/composer.json | 4 +- .../Debug/TraceableEventDispatcher.php | 335 +--------------- .../Debug/TraceableEventDispatcherTest.php | 152 -------- 8 files changed, 562 insertions(+), 480 deletions(-) create mode 100644 src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php create mode 100644 src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index ce5d7726279a3..f3b76d52c6abd 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 067c6c233f14c..bb42ee19c04ca 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 0000000000000..fc7bcb4dd601b --- /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 a67a979014f9b..5483e815068c4 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 0000000000000..8ccfabb1ca407 --- /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 054b8131c1d9c..6343b5d1d9c74 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 4c28abef61648..ed2417be721b0 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 d30837d2c6855..0b4af59d59fa3 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'); - } -} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy