PageRenderTime 27ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php

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