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