1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\EventDispatcher\Debug; 13 14use Psr\EventDispatcher\StoppableEventInterface; 15use Psr\Log\LoggerInterface; 16use Symfony\Component\EventDispatcher\EventDispatcherInterface; 17use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18use Symfony\Component\HttpFoundation\Request; 19use Symfony\Component\HttpFoundation\RequestStack; 20use Symfony\Component\Stopwatch\Stopwatch; 21use Symfony\Contracts\Service\ResetInterface; 22 23/** 24 * Collects some data about event listeners. 25 * 26 * This event dispatcher delegates the dispatching to another one. 27 * 28 * @author Fabien Potencier <fabien@symfony.com> 29 */ 30class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterface 31{ 32 protected $logger; 33 protected $stopwatch; 34 35 private $callStack; 36 private $dispatcher; 37 private $wrappedListeners; 38 private $orphanedEvents; 39 private $requestStack; 40 private $currentRequestHash = ''; 41 42 public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null, RequestStack $requestStack = null) 43 { 44 $this->dispatcher = $dispatcher; 45 $this->stopwatch = $stopwatch; 46 $this->logger = $logger; 47 $this->wrappedListeners = []; 48 $this->orphanedEvents = []; 49 $this->requestStack = $requestStack; 50 } 51 52 /** 53 * {@inheritdoc} 54 */ 55 public function addListener(string $eventName, $listener, int $priority = 0) 56 { 57 $this->dispatcher->addListener($eventName, $listener, $priority); 58 } 59 60 /** 61 * {@inheritdoc} 62 */ 63 public function addSubscriber(EventSubscriberInterface $subscriber) 64 { 65 $this->dispatcher->addSubscriber($subscriber); 66 } 67 68 /** 69 * {@inheritdoc} 70 */ 71 public function removeListener(string $eventName, $listener) 72 { 73 if (isset($this->wrappedListeners[$eventName])) { 74 foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { 75 if ($wrappedListener->getWrappedListener() === $listener) { 76 $listener = $wrappedListener; 77 unset($this->wrappedListeners[$eventName][$index]); 78 break; 79 } 80 } 81 } 82 83 return $this->dispatcher->removeListener($eventName, $listener); 84 } 85 86 /** 87 * {@inheritdoc} 88 */ 89 public function removeSubscriber(EventSubscriberInterface $subscriber) 90 { 91 return $this->dispatcher->removeSubscriber($subscriber); 92 } 93 94 /** 95 * {@inheritdoc} 96 */ 97 public function getListeners(string $eventName = null) 98 { 99 return $this->dispatcher->getListeners($eventName); 100 } 101 102 /** 103 * {@inheritdoc} 104 */ 105 public function getListenerPriority(string $eventName, $listener) 106 { 107 // we might have wrapped listeners for the event (if called while dispatching) 108 // in that case get the priority by wrapper 109 if (isset($this->wrappedListeners[$eventName])) { 110 foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { 111 if ($wrappedListener->getWrappedListener() === $listener) { 112 return $this->dispatcher->getListenerPriority($eventName, $wrappedListener); 113 } 114 } 115 } 116 117 return $this->dispatcher->getListenerPriority($eventName, $listener); 118 } 119 120 /** 121 * {@inheritdoc} 122 */ 123 public function hasListeners(string $eventName = null) 124 { 125 return $this->dispatcher->hasListeners($eventName); 126 } 127 128 /** 129 * {@inheritdoc} 130 */ 131 public function dispatch(object $event, string $eventName = null): object 132 { 133 $eventName = $eventName ?? \get_class($event); 134 135 if (null === $this->callStack) { 136 $this->callStack = new \SplObjectStorage(); 137 } 138 139 $currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; 140 141 if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) { 142 $this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName)); 143 } 144 145 $this->preProcess($eventName); 146 try { 147 $this->beforeDispatch($eventName, $event); 148 try { 149 $e = $this->stopwatch->start($eventName, 'section'); 150 try { 151 $this->dispatcher->dispatch($event, $eventName); 152 } finally { 153 if ($e->isStarted()) { 154 $e->stop(); 155 } 156 } 157 } finally { 158 $this->afterDispatch($eventName, $event); 159 } 160 } finally { 161 $this->currentRequestHash = $currentRequestHash; 162 $this->postProcess($eventName); 163 } 164 165 return $event; 166 } 167 168 /** 169 * @return array 170 */ 171 public function getCalledListeners(Request $request = null) 172 { 173 if (null === $this->callStack) { 174 return []; 175 } 176 177 $hash = $request ? spl_object_hash($request) : null; 178 $called = []; 179 foreach ($this->callStack as $listener) { 180 [$eventName, $requestHash] = $this->callStack->getInfo(); 181 if (null === $hash || $hash === $requestHash) { 182 $called[] = $listener->getInfo($eventName); 183 } 184 } 185 186 return $called; 187 } 188 189 /** 190 * @return array 191 */ 192 public function getNotCalledListeners(Request $request = null) 193 { 194 try { 195 $allListeners = $this->getListeners(); 196 } catch (\Exception $e) { 197 if (null !== $this->logger) { 198 $this->logger->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]); 199 } 200 201 // unable to retrieve the uncalled listeners 202 return []; 203 } 204 205 $hash = $request ? spl_object_hash($request) : null; 206 $calledListeners = []; 207 208 if (null !== $this->callStack) { 209 foreach ($this->callStack as $calledListener) { 210 [, $requestHash] = $this->callStack->getInfo(); 211 212 if (null === $hash || $hash === $requestHash) { 213 $calledListeners[] = $calledListener->getWrappedListener(); 214 } 215 } 216 } 217 218 $notCalled = []; 219 foreach ($allListeners as $eventName => $listeners) { 220 foreach ($listeners as $listener) { 221 if (!\in_array($listener, $calledListeners, true)) { 222 if (!$listener instanceof WrappedListener) { 223 $listener = new WrappedListener($listener, null, $this->stopwatch, $this); 224 } 225 $notCalled[] = $listener->getInfo($eventName); 226 } 227 } 228 } 229 230 uasort($notCalled, [$this, 'sortNotCalledListeners']); 231 232 return $notCalled; 233 } 234 235 public function getOrphanedEvents(Request $request = null): array 236 { 237 if ($request) { 238 return $this->orphanedEvents[spl_object_hash($request)] ?? []; 239 } 240 241 if (!$this->orphanedEvents) { 242 return []; 243 } 244 245 return array_merge(...array_values($this->orphanedEvents)); 246 } 247 248 public function reset() 249 { 250 $this->callStack = null; 251 $this->orphanedEvents = []; 252 $this->currentRequestHash = ''; 253 } 254 255 /** 256 * Proxies all method calls to the original event dispatcher. 257 * 258 * @param string $method The method name 259 * @param array $arguments The method arguments 260 * 261 * @return mixed 262 */ 263 public function __call(string $method, array $arguments) 264 { 265 return $this->dispatcher->{$method}(...$arguments); 266 } 267 268 /** 269 * Called before dispatching the event. 270 */ 271 protected function beforeDispatch(string $eventName, object $event) 272 { 273 } 274 275 /** 276 * Called after dispatching the event. 277 */ 278 protected function afterDispatch(string $eventName, object $event) 279 { 280 } 281 282 private function preProcess(string $eventName): void 283 { 284 if (!$this->dispatcher->hasListeners($eventName)) { 285 $this->orphanedEvents[$this->currentRequestHash][] = $eventName; 286 287 return; 288 } 289 290 foreach ($this->dispatcher->getListeners($eventName) as $listener) { 291 $priority = $this->getListenerPriority($eventName, $listener); 292 $wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this); 293 $this->wrappedListeners[$eventName][] = $wrappedListener; 294 $this->dispatcher->removeListener($eventName, $listener); 295 $this->dispatcher->addListener($eventName, $wrappedListener, $priority); 296 $this->callStack->attach($wrappedListener, [$eventName, $this->currentRequestHash]); 297 } 298 } 299 300 private function postProcess(string $eventName): void 301 { 302 unset($this->wrappedListeners[$eventName]); 303 $skipped = false; 304 foreach ($this->dispatcher->getListeners($eventName) as $listener) { 305 if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch. 306 continue; 307 } 308 // Unwrap listener 309 $priority = $this->getListenerPriority($eventName, $listener); 310 $this->dispatcher->removeListener($eventName, $listener); 311 $this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority); 312 313 if (null !== $this->logger) { 314 $context = ['event' => $eventName, 'listener' => $listener->getPretty()]; 315 } 316 317 if ($listener->wasCalled()) { 318 if (null !== $this->logger) { 319 $this->logger->debug('Notified event "{event}" to listener "{listener}".', $context); 320 } 321 } else { 322 $this->callStack->detach($listener); 323 } 324 325 if (null !== $this->logger && $skipped) { 326 $this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context); 327 } 328 329 if ($listener->stoppedPropagation()) { 330 if (null !== $this->logger) { 331 $this->logger->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context); 332 } 333 334 $skipped = true; 335 } 336 } 337 } 338 339 private function sortNotCalledListeners(array $a, array $b) 340 { 341 if (0 !== $cmp = strcmp($a['event'], $b['event'])) { 342 return $cmp; 343 } 344 345 if (\is_int($a['priority']) && !\is_int($b['priority'])) { 346 return 1; 347 } 348 349 if (!\is_int($a['priority']) && \is_int($b['priority'])) { 350 return -1; 351 } 352 353 if ($a['priority'] === $b['priority']) { 354 return 0; 355 } 356 357 if ($a['priority'] > $b['priority']) { 358 return -1; 359 } 360 361 return 1; 362 } 363} 364