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