TraceableEventDispatcher.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  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\HttpKernel\Debug;
  11. use Symfony\Component\Stopwatch\Stopwatch;
  12. use Symfony\Component\HttpKernel\KernelEvents;
  13. use Psr\Log\LoggerInterface;
  14. use Symfony\Component\HttpKernel\Profiler\Profile;
  15. use Symfony\Component\HttpKernel\Profiler\Profiler;
  16. use Symfony\Component\HttpKernel\HttpKernelInterface;
  17. use Symfony\Component\EventDispatcher\Event;
  18. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  19. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  20. use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface;
  21. /**
  22. * Collects some data about event listeners.
  23. *
  24. * This event dispatcher delegates the dispatching to another one.
  25. *
  26. * @author Fabien Potencier <fabien@symfony.com>
  27. */
  28. class TraceableEventDispatcher implements EventDispatcherInterface, TraceableEventDispatcherInterface
  29. {
  30. private $logger;
  31. private $called;
  32. private $stopwatch;
  33. private $profiler;
  34. private $dispatcher;
  35. private $wrappedListeners;
  36. private $firstCalledEvent;
  37. private $id;
  38. /**
  39. * Constructor.
  40. *
  41. * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance
  42. * @param Stopwatch $stopwatch A Stopwatch instance
  43. * @param LoggerInterface $logger A LoggerInterface instance
  44. */
  45. public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null)
  46. {
  47. $this->dispatcher = $dispatcher;
  48. $this->stopwatch = $stopwatch;
  49. $this->logger = $logger;
  50. $this->called = array();
  51. $this->wrappedListeners = array();
  52. $this->firstCalledEvent = array();
  53. }
  54. /**
  55. * Sets the profiler.
  56. *
  57. * @param Profiler|null $profiler A Profiler instance
  58. */
  59. public function setProfiler(Profiler $profiler = null)
  60. {
  61. $this->profiler = $profiler;
  62. }
  63. /**
  64. * {@inheritDoc}
  65. */
  66. public function addListener($eventName, $listener, $priority = 0)
  67. {
  68. $this->dispatcher->addListener($eventName, $listener, $priority);
  69. }
  70. /**
  71. * {@inheritdoc}
  72. */
  73. public function addSubscriber(EventSubscriberInterface $subscriber)
  74. {
  75. $this->dispatcher->addSubscriber($subscriber);
  76. }
  77. /**
  78. * {@inheritdoc}
  79. */
  80. public function removeListener($eventName, $listener)
  81. {
  82. return $this->dispatcher->removeListener($eventName, $listener);
  83. }
  84. /**
  85. * {@inheritdoc}
  86. */
  87. public function removeSubscriber(EventSubscriberInterface $subscriber)
  88. {
  89. return $this->dispatcher->removeSubscriber($subscriber);
  90. }
  91. /**
  92. * {@inheritdoc}
  93. */
  94. public function getListeners($eventName = null)
  95. {
  96. return $this->dispatcher->getListeners($eventName);
  97. }
  98. /**
  99. * {@inheritdoc}
  100. */
  101. public function hasListeners($eventName = null)
  102. {
  103. return $this->dispatcher->hasListeners($eventName);
  104. }
  105. /**
  106. * {@inheritdoc}
  107. */
  108. public function dispatch($eventName, Event $event = null)
  109. {
  110. if (null === $event) {
  111. $event = new Event();
  112. }
  113. $this->id = spl_object_hash($event);
  114. $this->preDispatch($eventName, $event);
  115. $e = $this->stopwatch->start($eventName, 'section');
  116. $this->firstCalledEvent[$eventName] = $this->stopwatch->start($eventName.'.loading', 'event_listener_loading');
  117. if (!$this->dispatcher->hasListeners($eventName)) {
  118. $this->firstCalledEvent[$eventName]->stop();
  119. }
  120. $this->dispatcher->dispatch($eventName, $event);
  121. // reset the id as another event might have been dispatched during the dispatching of this event
  122. $this->id = spl_object_hash($event);
  123. unset($this->firstCalledEvent[$eventName]);
  124. $e->stop();
  125. $this->postDispatch($eventName, $event);
  126. return $event;
  127. }
  128. /**
  129. * {@inheritDoc}
  130. */
  131. public function getCalledListeners()
  132. {
  133. return $this->called;
  134. }
  135. /**
  136. * {@inheritDoc}
  137. */
  138. public function getNotCalledListeners()
  139. {
  140. $notCalled = array();
  141. foreach ($this->getListeners() as $name => $listeners) {
  142. foreach ($listeners as $listener) {
  143. $info = $this->getListenerInfo($listener, $name);
  144. if (!isset($this->called[$name.'.'.$info['pretty']])) {
  145. $notCalled[$name.'.'.$info['pretty']] = $info;
  146. }
  147. }
  148. }
  149. return $notCalled;
  150. }
  151. /**
  152. * Proxies all method calls to the original event dispatcher.
  153. *
  154. * @param string $method The method name
  155. * @param array $arguments The method arguments
  156. *
  157. * @return mixed
  158. */
  159. public function __call($method, $arguments)
  160. {
  161. return call_user_func_array(array($this->dispatcher, $method), $arguments);
  162. }
  163. /**
  164. * This is a private method and must not be used.
  165. *
  166. * This method is public because it is used in a closure.
  167. * Whenever Symfony will require PHP 5.4, this could be changed
  168. * to a proper private method.
  169. */
  170. public function logSkippedListeners($eventName, Event $event, $listener)
  171. {
  172. if (null === $this->logger) {
  173. return;
  174. }
  175. $info = $this->getListenerInfo($listener, $eventName);
  176. $this->logger->debug(sprintf('Listener "%s" stopped propagation of the event "%s".', $info['pretty'], $eventName));
  177. $skippedListeners = $this->getListeners($eventName);
  178. $skipped = false;
  179. foreach ($skippedListeners as $skippedListener) {
  180. $skippedListener = $this->unwrapListener($skippedListener);
  181. if ($skipped) {
  182. $info = $this->getListenerInfo($skippedListener, $eventName);
  183. $this->logger->debug(sprintf('Listener "%s" was not called for event "%s".', $info['pretty'], $eventName));
  184. }
  185. if ($skippedListener === $listener) {
  186. $skipped = true;
  187. }
  188. }
  189. }
  190. /**
  191. * This is a private method.
  192. *
  193. * This method is public because it is used in a closure.
  194. * Whenever Symfony will require PHP 5.4, this could be changed
  195. * to a proper private method.
  196. */
  197. public function preListenerCall($eventName, $listener)
  198. {
  199. // is it the first called listener?
  200. if (isset($this->firstCalledEvent[$eventName])) {
  201. $this->firstCalledEvent[$eventName]->stop();
  202. unset($this->firstCalledEvent[$eventName]);
  203. }
  204. $info = $this->getListenerInfo($listener, $eventName);
  205. if (null !== $this->logger) {
  206. $this->logger->debug(sprintf('Notified event "%s" to listener "%s".', $eventName, $info['pretty']));
  207. }
  208. $this->called[$eventName.'.'.$info['pretty']] = $info;
  209. return $this->stopwatch->start(isset($info['class']) ? $info['class'] : $info['type'], 'event_listener');
  210. }
  211. /**
  212. * Returns information about the listener
  213. *
  214. * @param object $listener The listener
  215. * @param string $eventName The event name
  216. *
  217. * @return array Informations about the listener
  218. */
  219. private function getListenerInfo($listener, $eventName)
  220. {
  221. $listener = $this->unwrapListener($listener);
  222. $info = array(
  223. 'event' => $eventName,
  224. );
  225. if ($listener instanceof \Closure) {
  226. $info += array(
  227. 'type' => 'Closure',
  228. 'pretty' => 'closure'
  229. );
  230. } elseif (is_string($listener)) {
  231. try {
  232. $r = new \ReflectionFunction($listener);
  233. $file = $r->getFileName();
  234. $line = $r->getStartLine();
  235. } catch (\ReflectionException $e) {
  236. $file = null;
  237. $line = null;
  238. }
  239. $info += array(
  240. 'type' => 'Function',
  241. 'function' => $listener,
  242. 'file' => $file,
  243. 'line' => $line,
  244. 'pretty' => $listener,
  245. );
  246. } elseif (is_array($listener) || (is_object($listener) && is_callable($listener))) {
  247. if (!is_array($listener)) {
  248. $listener = array($listener, '__invoke');
  249. }
  250. $class = is_object($listener[0]) ? get_class($listener[0]) : $listener[0];
  251. try {
  252. $r = new \ReflectionMethod($class, $listener[1]);
  253. $file = $r->getFileName();
  254. $line = $r->getStartLine();
  255. } catch (\ReflectionException $e) {
  256. $file = null;
  257. $line = null;
  258. }
  259. $info += array(
  260. 'type' => 'Method',
  261. 'class' => $class,
  262. 'method' => $listener[1],
  263. 'file' => $file,
  264. 'line' => $line,
  265. 'pretty' => $class.'::'.$listener[1],
  266. );
  267. }
  268. return $info;
  269. }
  270. /**
  271. * Updates the stopwatch data in the profile hierarchy.
  272. *
  273. * @param string $token Profile token
  274. * @param Boolean $updateChildren Whether to update the children altogether
  275. */
  276. private function updateProfiles($token, $updateChildren)
  277. {
  278. if (!$this->profiler || !$profile = $this->profiler->loadProfile($token)) {
  279. return;
  280. }
  281. $this->saveInfoInProfile($profile, $updateChildren);
  282. }
  283. /**
  284. * Update the profiles with the timing and events information and saves them.
  285. *
  286. * @param Profile $profile The root profile
  287. * @param Boolean $updateChildren Whether to update the children altogether
  288. */
  289. private function saveInfoInProfile(Profile $profile, $updateChildren)
  290. {
  291. try {
  292. $collector = $profile->getCollector('memory');
  293. $collector->updateMemoryUsage();
  294. } catch (\InvalidArgumentException $e) {
  295. }
  296. try {
  297. $collector = $profile->getCollector('time');
  298. $collector->setEvents($this->stopwatch->getSectionEvents($profile->getToken()));
  299. } catch (\InvalidArgumentException $e) {
  300. }
  301. try {
  302. $collector = $profile->getCollector('events');
  303. $collector->setCalledListeners($this->getCalledListeners());
  304. $collector->setNotCalledListeners($this->getNotCalledListeners());
  305. } catch (\InvalidArgumentException $e) {
  306. }
  307. $this->profiler->saveProfile($profile);
  308. if ($updateChildren) {
  309. foreach ($profile->getChildren() as $child) {
  310. $this->saveInfoInProfile($child, true);
  311. }
  312. }
  313. }
  314. private function preDispatch($eventName, Event $event)
  315. {
  316. // wrap all listeners before they are called
  317. $this->wrappedListeners[$this->id] = new \SplObjectStorage();
  318. $listeners = $this->dispatcher->getListeners($eventName);
  319. foreach ($listeners as $listener) {
  320. $this->dispatcher->removeListener($eventName, $listener);
  321. $wrapped = $this->wrapListener($eventName, $listener);
  322. $this->wrappedListeners[$this->id][$wrapped] = $listener;
  323. $this->dispatcher->addListener($eventName, $wrapped);
  324. }
  325. switch ($eventName) {
  326. case KernelEvents::REQUEST:
  327. $this->stopwatch->openSection();
  328. break;
  329. case KernelEvents::VIEW:
  330. case KernelEvents::RESPONSE:
  331. // stop only if a controller has been executed
  332. if ($this->stopwatch->isStarted('controller')) {
  333. $this->stopwatch->stop('controller');
  334. }
  335. break;
  336. case KernelEvents::TERMINATE:
  337. $token = $event->getResponse()->headers->get('X-Debug-Token');
  338. // There is a very special case when using builtin AppCache class as kernel wrapper, in the case
  339. // of an ESI request leading to a `stale` response [B] inside a `fresh` cached response [A].
  340. // In this case, `$token` contains the [B] debug token, but the open `stopwatch` section ID
  341. // is equal to the [A] debug token. Trying to reopen section with the [B] token throws an exception
  342. // which must be caught.
  343. try {
  344. $this->stopwatch->openSection($token);
  345. } catch (\LogicException $e) {}
  346. break;
  347. }
  348. }
  349. private function postDispatch($eventName, Event $event)
  350. {
  351. switch ($eventName) {
  352. case KernelEvents::CONTROLLER:
  353. $this->stopwatch->start('controller', 'section');
  354. break;
  355. case KernelEvents::RESPONSE:
  356. $token = $event->getResponse()->headers->get('X-Debug-Token');
  357. $this->stopwatch->stopSection($token);
  358. if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) {
  359. // The profiles can only be updated once they have been created
  360. // that is after the 'kernel.response' event of the main request
  361. $this->updateProfiles($token, true);
  362. }
  363. break;
  364. case KernelEvents::TERMINATE:
  365. $token = $event->getResponse()->headers->get('X-Debug-Token');
  366. // In the special case described in the `preDispatch` method above, the `$token` section
  367. // does not exist, then closing it throws an exception which must be caught.
  368. try {
  369. $this->stopwatch->stopSection($token);
  370. } catch (\LogicException $e) {}
  371. // The children profiles have been updated by the previous 'kernel.response'
  372. // event. Only the root profile need to be updated with the 'kernel.terminate'
  373. // timing informations.
  374. $this->updateProfiles($token, false);
  375. break;
  376. }
  377. foreach ($this->wrappedListeners[$this->id] as $wrapped) {
  378. $this->dispatcher->removeListener($eventName, $wrapped);
  379. $this->dispatcher->addListener($eventName, $this->wrappedListeners[$this->id][$wrapped]);
  380. }
  381. unset($this->wrappedListeners[$this->id]);
  382. }
  383. private function wrapListener($eventName, $listener)
  384. {
  385. $self = $this;
  386. return function (Event $event) use ($self, $eventName, $listener) {
  387. $e = $self->preListenerCall($eventName, $listener);
  388. call_user_func($listener, $event);
  389. $e->stop();
  390. if ($event->isPropagationStopped()) {
  391. $self->logSkippedListeners($eventName, $event, $listener);
  392. }
  393. };
  394. }
  395. private function unwrapListener($listener)
  396. {
  397. // get the original listener
  398. if (is_object($listener) && isset($this->wrappedListeners[$this->id][$listener])) {
  399. return $this->wrappedListeners[$this->id][$listener];
  400. }
  401. return $listener;
  402. }
  403. }