TraceableEventDispatcher.php 13 KB

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