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