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