8000 feature #9814 [EventDispatcher] Added TraceableEventDispatcher from H… · symfony/symfony@f063108 · GitHub
[go: up one dir, main page]

Skip to content

Commit f063108

Browse files
committed
feature #9814 [EventDispatcher] Added TraceableEventDispatcher from HttpKernel (florianv)
This PR was merged into the 2.5-dev branch. Discussion ---------- [EventDispatcher] Added TraceableEventDispatcher from HttpKernel | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | BC breaks? | yes | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | Moved the reusable parts from the `TraceableEventDispatcher` from `HttpKernel` to the same class in `EventDispatcher` to allow debugging dispatchers. I changed the `TraceableEventDispatcherInterface` to extend `EventDispatcherInterface`, so this is a BC break but I'm not sure if a lot of people use it without actually extending it. Commits ------- 9a90e06 [EventDispatcher] Added TraceableEventDispatcher from HttpKernel
2 parents 0b0c431 + 9a90e06 commit f063108

File tree

8 files changed

+562
-480
lines changed

8 files changed

+562
-480
lines changed

UPGRADE-3.0.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ UPGRADE FROM 2.x to 3.0
1818
`DebugClassLoader`. The difference is that the constructor now takes a
1919
loader to wrap.
2020

21+
### EventDispatcher
22+
23+
* The interface `Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface`
24+
extends `Symfony\Component\EventDispatcher\EventDispatcherInterface`.
25+
2126
### Form
2227

2328
* The methods `Form::bind()` and `Form::isBound()` were removed. You should

src/Symfony/Component/EventDispatcher/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
< 6855 div class="border position-relative rounded-bottom-2">
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ CHANGELOG
44
2.5.0
55
-----
66

7+
* added Debug\TraceableEventDispatcher (originally in HttpKernel)
8+
* changed Debug\TraceableEventDispatcherInterface to extend EventDispatcherInterface
79
* added RegisterListenersPass (originally in HttpKernel)
810

911
2.1.0
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
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+
12+
namespace Symfony\Component\EventDispatcher\Debug;
13+
14+
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface;
15+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
16+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17+
use Symfony\Component\EventDispatcher\Event;
18+
use Symfony\Component\Stopwatch\Stopwatch;
19+
use Psr\Log\LoggerInterface;
20+
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 TraceableEventDispatcherInterface
29+
{
30+
protected $logger;
31+
protected $stopwatch;
32+
private $called = array();
33+
private $dispatcher;
34+
private $wrappedListeners = array();
35+
private $firstCalledEvent = array();
36+
private $id;
37+
private $lastEventId = 0;
38+
39+
/**
40+
* Constructor.
41+
*
42+
* @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance
43+
* @param Stopwatch $stopwatch A Stopwatch instance
44+
* @param LoggerInterface $logger A LoggerInterface instance
45+
*/
46+
public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null)
47+
{
48+
$this->dispatcher = $dispatcher;
49+
$this->stopwatch = $stopwatch;
50+
$this->logger = $logger;
51+
}
52+
53+
/**
54+
* {@inheritDoc}
55+
*/
56+
public function addListener($eventName, $listener, $priority = 0)
57+
{
58+
$this->dispatcher->addListener($eventName, $listener, $priority);
59+
}
60+
61+
/**
62+
* {@inheritdoc}
63+
*/
64+
public function addSubscriber(EventSubscriberInterface $subscriber)
65+
{
66+
$this->dispatcher->addSubscriber($subscriber);
67+
}
68+
69+
/**
70+
* {@inheritdoc}
71+
*/
72+
public function removeListener($eventName, $listener)
73+
{
74+
return $this->dispatcher->removeListener($eventName, $listener);
75+
}
76+
77+
/**
78+
* {@inheritdoc}
79+
*/
80+
public function removeSubscriber(EventSubscriberInterface $subscriber)
81+
{
82+
return $this->dispatcher->removeSubscriber($subscriber);
83+
}
84+
85+
/**
86+
* {@inheritdoc}
87+
*/
88+
public function getListeners($eventName = null)
89+
{
90+
return $this->dispatcher->getListeners($eventName);
91+
}
92+
93+
/**
94+
* {@inheritdoc}
95+
*/
96+
public function hasListeners($eventName = null)
97+
{
98+
return $this->dispatcher->hasListeners($eventName);
99+
}
100+
101+
/**
102+
* {@inheritdoc}
103+
*/
104+
public function dispatch($eventName, Event $event = null)
105+
{
106+
if (null === $event) {
107+
$event = new Event();
108+
}
109+
110+
$this->id = $eventId = ++$this->lastEventId;
111+
112+
// Wrap all listeners before they are called
113+
$this->wrappedListeners[$this->id] = new \SplObjectStorage();
114+
115+
$listeners = $this->dispatcher->getListeners($eventName);
116+
117+
foreach ($listeners as $listener) {
118+
$this->dispatcher->removeListener($eventName, $listener);
119+
$wrapped = $this->wrapListener($eventName, $listener);
120+
$this->wrappedListeners[$this->id][$wrapped] = $listener;
121+
$this->dispatcher->addListener($eventName, $wrapped);
122+
}
123+
124+
$this->preDispatch($eventName, $event);
125+
126+
$e = $this->stopwatch->start($eventName, 'section');
127+
128+
$this->firstCalledEvent[$eventName] = $this->stopwatch->start($eventName.'.loading', 'event_listener_loading');
129+
130+
if (!$this->dispatcher->hasListeners($eventName)) {
131+
$this->firstCalledEvent[$eventName]->stop();
132+
}
133+
134+
$this->dispatcher->dispatch($eventName, $event);
135+
136+
// reset the id as another event might have been dispatched during the dispatching of this event
137+
$this->id = $eventId;
138+
139+
unset($this->firstCalledEvent[$eventName]);
140+
141+
if ($e->isStarted()) {
142+
$e->stop();
143+
}
144+
145+
$this->postDispatch($eventName, $event);
146+
147+
// Unwrap all listeners after they are called
148+
foreach ($this->wrappedListeners[$this->id] as $wrapped) {
149+
$this->dispatcher->removeListener($eventName, $wrapped);
150+
$this->dispatcher->addListener($eventName, $this->wrappedListeners[$this->id][$wrapped]);
151+
}
152+
153+
unset($this->wrappedListeners[$this->id]);
154+
155+
return $event;
156+
}
157+
158+
/**
159+
* {@inheritDoc}
160+
*/
161+
public function getCalledListeners()
162+
{
163+
return $this->called;
164+
}
165+
166+
/**
167+
* {@inheritDoc}
168+
*/
169+
public function getNotCalledListeners()
170+
{
171+
$notCalled = array();
172+
173+
foreach ($this->getListeners() as $name => $listeners) {
174+
foreach ($listeners as $listener) {
175+
$info = $this->getListenerInfo($listener, $name);
176+
if (!isset($this->called[$name.'.'.$info['pretty']])) {
177+
$notCalled[$name.'.'.$info['pretty']] = $info;
178+
}
179+
}
180+
}
181+
182+
return $notCalled;
183+
}
184+
185+
/**
186+
* Proxies all method calls to the original event dispatcher.
187+
*
188+
* @param string $method The method name
189+
* @param array $arguments The method arguments
190+
*
191+
* @return mixed
192+
*/
193+
public function __call($method, $arguments)
194+
{
195+
return call_user_func_array(array($this->dispatcher, $method), $arguments);
196+
}
197+
198+
/**
199+
* This is a private method and must not be used.
200+
*
201+
* This method is public because it is used in a closure.
202+
* Whenever Symfony will require PHP 5.4, this could be changed
203+
* to a proper private method.
204+
*/
205+
public function logSkippedListeners($eventName, Event $event, $listener)
206+
{
207+
if (null === $this->logger) {
208+
return;
209+
}
210+
211+
$info = $this->getListenerInfo($listener, $eventName);
212+
213+
$this->logger->debug(sprintf('Listener "%s" stopped propagation of the event "%s".', $info['pretty'], $eventName));
214+
215+
$skippedListeners = $this->getListeners($eventName);
216+
$skipped = false;
217+
218+
foreach ($skippedListeners as $skippedListener) {
219+
$skippedListener = $this->unwrapListener($skippedListener);
220+
221+
if ($skipped) {
222+
$info = $this->getListenerInfo($skippedListener, $eventName);
223+
$this->logger->debug(sprintf('Listener "%s" was not called for event "%s".', $info['pretty'], $eventName));
224+
}
225+
226+
if ($skippedListener === $listener) {
227+
$skipped = true;
228+
}
229+
}
230+
}
231+
232+
/**
233+
* This is a private method.
234+
*
235+
* This method is public because it is used in a closure.
236+
* Whenever Symfony will require PHP 5.4, this could be changed
237+
* to a proper private method.
238+
*/
239+
public function preListenerCall($eventName, $listener)
240+
{
241+
// is it the first called listener?
242+
if (isset($this->firstCalledEvent[$eventName])) {
243+
$this->firstCalledEvent[$eventName]->stop();
244+
245+
unset($this->firstCalledEvent[$eventName]);
246+
}
247+
248+
$info = $this->getListenerInfo($listener, $eventName);
249+
250+
if (null !== $this->logger) {
251+
$this->logger->debug(sprintf('Notified event "%s" to listener "%s".', $eventName, $info['pretty']));
252+
}
253+
254+
$this->called[$eventName.'.'.$info['pretty']] = $info;
255+
256+
return $this->stopwatch->start(isset($info['class']) ? $info['class'] : $info['type'], 'event_listener');
257+
}
258+
259+
/**
260+
* Returns information about the listener
261+
*
262+
* @param object $listener The listener
263+
* @param string $eventName The event name
264+
*
265+
* @return array Informations about the listener
266+
*/
267+
private function getListenerInfo($listener, $eventName)
268+
{
269+
$listener = $this->unwrapListener($listener);
270+
271+
$info = array(
272+
'event' => $eventName,
273+
);
274+
if ($listener instanceof \Closure) {
275+
$info += array(
276+
'type' => 'Closure',
277+
'pretty' => 'closure'
278+
);
279+
} elseif (is_string($listener)) {
280+
try {
281+
$r = new \ReflectionFunction($listener);
282+
$file = $r->getFileName();
283+
$line = $r->getStartLine();
284+
} catch (\ReflectionException $e) {
285+
$file = null;
286+
$line = null;
287+
}
288+
$info += array(
289+
'type' => 'Function',
290+
'function' => $listener,
291+
'file' => $file,
292+
'line' => $line,
293+
'pretty' => $listener,
294+
);
295+
} elseif (is_array($listener) || (is_object($listener) && is_callable($listener))) {
296+
if (!is_array($listener)) {
297+
$listener = array($listener, '__invoke');
298+
}
299+
$class = is_object($listener[0]) ? get_class($listener[0]) : $listener[0];
300+
try {
301+
$r = new \ReflectionMethod($class, $listener[1]);
302+
$file = $r->getFileName();
303+
$line = $r->getStartLine();
304+
} catch (\ReflectionException $e) {
305+
$file = null;
306+
$line = null;
307+
}
308+
$info += array(
309+
'type' => 'Method',
310+
'class' => $class,
311+
'method' => $listener[1],
312+
'file' => $file,
313+
'line' => $line,
314+
'pretty' => $class.'::'.$listener[1],
315+
);
316+
}
317+
318+
return $info;
319+
}
320+
321+
/**
322+
* Called before dispatching the event.
323+
*
324+
* @param string $eventName The event name
325+
* @param Event $event The event
326+
*/
327+
protected function preDispatch($eventName, Event $event)
328+
{
329+
}
330+
331+
/**
332+
* Called after dispatching the event.
333+
*
334+
* @param string $eventName The event name
335+
* @param Event $event The event
336+
*/
337+
protected function postDispatch($eventName, Event $event)
338+
{
339+
}
340+
341+
private function wrapListener($eventName, $listener)
342+
{
343+
$self = $this;
344+
345+
return function (Event $event) use ($self, $eventName, $listener) {
346+
$e = $self->preListenerCall($eventName, $listener);
347+
348+
call_user_func($listener, $event, $eventName, $self);
349+
350+
if ($e->isStarted()) {
351+
$e->stop();
352+
}
353+
354+
if ($event->isPropagationStopped()) {
355+
$self->logSkippedListeners($eventName, $event, $listener);
356+
}
357+
};
358+
}
359+
360+
private function unwrapListener($listener)
361+
{
362+
// get the original listener
363+
if (is_object($listener) && isset($this->wrappedListeners[$this->id][$listener])) {
364+
return $this->wrappedListeners[$this->id][$listener];
365+
}
366+
367+
return $listener;
368+
}
369+
}

0 commit comments

Comments
 (0)
0