| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 | <?php/* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */namespace Symfony\Component\EventDispatcher\Debug;use Psr\EventDispatcher\StoppableEventInterface;use Psr\Log\LoggerInterface;use Symfony\Component\EventDispatcher\Event;use Symfony\Component\EventDispatcher\EventDispatcherInterface;use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;use Symfony\Component\EventDispatcher\LegacyEventProxy;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\RequestStack;use Symfony\Component\Stopwatch\Stopwatch;use Symfony\Contracts\EventDispatcher\Event as ContractsEvent;/** * Collects some data about event listeners. * * This event dispatcher delegates the dispatching to another one. * * @author Fabien Potencier <fabien@symfony.com> */class TraceableEventDispatcher implements TraceableEventDispatcherInterface{    protected $logger;    protected $stopwatch;    private $callStack;    private $dispatcher;    private $wrappedListeners;    private $orphanedEvents;    private $requestStack;    private $currentRequestHash = '';    public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null, RequestStack $requestStack = null)    {        $this->dispatcher = LegacyEventDispatcherProxy::decorate($dispatcher);        $this->stopwatch = $stopwatch;        $this->logger = $logger;        $this->wrappedListeners = [];        $this->orphanedEvents = [];        $this->requestStack = $requestStack;    }    /**     * {@inheritdoc}     */    public function addListener($eventName, $listener, $priority = 0)    {        $this->dispatcher->addListener($eventName, $listener, $priority);    }    /**     * {@inheritdoc}     */    public function addSubscriber(EventSubscriberInterface $subscriber)    {        $this->dispatcher->addSubscriber($subscriber);    }    /**     * {@inheritdoc}     */    public function removeListener($eventName, $listener)    {        if (isset($this->wrappedListeners[$eventName])) {            foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) {                if ($wrappedListener->getWrappedListener() === $listener) {                    $listener = $wrappedListener;                    unset($this->wrappedListeners[$eventName][$index]);                    break;                }            }        }        return $this->dispatcher->removeListener($eventName, $listener);    }    /**     * {@inheritdoc}     */    public function removeSubscriber(EventSubscriberInterface $subscriber)    {        return $this->dispatcher->removeSubscriber($subscriber);    }    /**     * {@inheritdoc}     */    public function getListeners($eventName = null)    {        return $this->dispatcher->getListeners($eventName);    }    /**     * {@inheritdoc}     */    public function getListenerPriority($eventName, $listener)    {        // we might have wrapped listeners for the event (if called while dispatching)        // in that case get the priority by wrapper        if (isset($this->wrappedListeners[$eventName])) {            foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) {                if ($wrappedListener->getWrappedListener() === $listener) {                    return $this->dispatcher->getListenerPriority($eventName, $wrappedListener);                }            }        }        return $this->dispatcher->getListenerPriority($eventName, $listener);    }    /**     * {@inheritdoc}     */    public function hasListeners($eventName = null)    {        return $this->dispatcher->hasListeners($eventName);    }    /**     * {@inheritdoc}     *     * @param string|null $eventName     */    public function dispatch($event/*, string $eventName = null*/)    {        if (null === $this->callStack) {            $this->callStack = new \SplObjectStorage();        }        $currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : '';        $eventName = 1 < \func_num_args() ? func_get_arg(1) : null;        if (\is_object($event)) {            $eventName = $eventName ?? \get_class($event);        } else {            @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);            $swap = $event;            $event = $eventName ?? new Event();            $eventName = $swap;            if (!$event instanceof Event) {                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)));            }        }        if (null !== $this->logger && ($event instanceof Event || $event instanceof ContractsEvent || $event instanceof StoppableEventInterface) && $event->isPropagationStopped()) {            $this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName));        }        $this->preProcess($eventName);        try {            $this->beforeDispatch($eventName, $event);            try {                $e = $this->stopwatch->start($eventName, 'section');                try {                    $this->dispatcher->dispatch($event, $eventName);                } finally {                    if ($e->isStarted()) {                        $e->stop();                    }                }            } finally {                $this->afterDispatch($eventName, $event);            }        } finally {            $this->currentRequestHash = $currentRequestHash;            $this->postProcess($eventName);        }        return $event;    }    /**     * {@inheritdoc}     *     * @param Request|null $request The request to get listeners for     */    public function getCalledListeners(/* Request $request = null */)    {        if (null === $this->callStack) {            return [];        }        $hash = 1 <= \func_num_args() && null !== ($request = func_get_arg(0)) ? spl_object_hash($request) : null;        $called = [];        foreach ($this->callStack as $listener) {            [$eventName, $requestHash] = $this->callStack->getInfo();            if (null === $hash || $hash === $requestHash) {                $called[] = $listener->getInfo($eventName);            }        }        return $called;    }    /**     * {@inheritdoc}     *     * @param Request|null $request The request to get listeners for     */    public function getNotCalledListeners(/* Request $request = null */)    {        try {            $allListeners = $this->getListeners();        } catch (\Exception $e) {            if (null !== $this->logger) {                $this->logger->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]);            }            // unable to retrieve the uncalled listeners            return [];        }        $hash = 1 <= \func_num_args() && null !== ($request = func_get_arg(0)) ? spl_object_hash($request) : null;        $calledListeners = [];        if (null !== $this->callStack) {            foreach ($this->callStack as $calledListener) {                [, $requestHash] = $this->callStack->getInfo();                if (null === $hash || $hash === $requestHash) {                    $calledListeners[] = $calledListener->getWrappedListener();                }            }        }        $notCalled = [];        foreach ($allListeners as $eventName => $listeners) {            foreach ($listeners as $listener) {                if (!\in_array($listener, $calledListeners, true)) {                    if (!$listener instanceof WrappedListener) {                        $listener = new WrappedListener($listener, null, $this->stopwatch, $this);                    }                    $notCalled[] = $listener->getInfo($eventName);                }            }        }        uasort($notCalled, [$this, 'sortNotCalledListeners']);        return $notCalled;    }    /**     * @param Request|null $request The request to get orphaned events for     */    public function getOrphanedEvents(/* Request $request = null */): array    {        if (1 <= \func_num_args() && null !== $request = func_get_arg(0)) {            return $this->orphanedEvents[spl_object_hash($request)] ?? [];        }        if (!$this->orphanedEvents) {            return [];        }        return array_merge(...array_values($this->orphanedEvents));    }    public function reset()    {        $this->callStack = null;        $this->orphanedEvents = [];        $this->currentRequestHash = '';    }    /**     * Proxies all method calls to the original event dispatcher.     *     * @param string $method    The method name     * @param array  $arguments The method arguments     *     * @return mixed     */    public function __call($method, $arguments)    {        return $this->dispatcher->{$method}(...$arguments);    }    /**     * Called before dispatching the event.     *     * @param object $event     */    protected function beforeDispatch(string $eventName, $event)    {        $this->preDispatch($eventName, $event instanceof Event ? $event : new LegacyEventProxy($event));    }    /**     * Called after dispatching the event.     *     * @param object $event     */    protected function afterDispatch(string $eventName, $event)    {        $this->postDispatch($eventName, $event instanceof Event ? $event : new LegacyEventProxy($event));    }    /**     * @deprecated since Symfony 4.3, will be removed in 5.0, use beforeDispatch instead     */    protected function preDispatch($eventName, Event $event)    {    }    /**     * @deprecated since Symfony 4.3, will be removed in 5.0, use afterDispatch instead     */    protected function postDispatch($eventName, Event $event)    {    }    private function preProcess(string $eventName)    {        if (!$this->dispatcher->hasListeners($eventName)) {            $this->orphanedEvents[$this->currentRequestHash][] = $eventName;            return;        }        foreach ($this->dispatcher->getListeners($eventName) as $listener) {            $priority = $this->getListenerPriority($eventName, $listener);            $wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this);            $this->wrappedListeners[$eventName][] = $wrappedListener;            $this->dispatcher->removeListener($eventName, $listener);            $this->dispatcher->addListener($eventName, $wrappedListener, $priority);            $this->callStack->attach($wrappedListener, [$eventName, $this->currentRequestHash]);        }    }    private function postProcess(string $eventName)    {        unset($this->wrappedListeners[$eventName]);        $skipped = false;        foreach ($this->dispatcher->getListeners($eventName) as $listener) {            if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch.                continue;            }            // Unwrap listener            $priority = $this->getListenerPriority($eventName, $listener);            $this->dispatcher->removeListener($eventName, $listener);            $this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority);            if (null !== $this->logger) {                $context = ['event' => $eventName, 'listener' => $listener->getPretty()];            }            if ($listener->wasCalled()) {                if (null !== $this->logger) {                    $this->logger->debug('Notified event "{event}" to listener "{listener}".', $context);                }            } else {                $this->callStack->detach($listener);            }            if (null !== $this->logger && $skipped) {                $this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context);            }            if ($listener->stoppedPropagation()) {                if (null !== $this->logger) {                    $this->logger->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context);                }                $skipped = true;            }        }    }    private function sortNotCalledListeners(array $a, array $b)    {        if (0 !== $cmp = strcmp($a['event'], $b['event'])) {            return $cmp;        }        if (\is_int($a['priority']) && !\is_int($b['priority'])) {            return 1;        }        if (!\is_int($a['priority']) && \is_int($b['priority'])) {            return -1;        }        if ($a['priority'] === $b['priority']) {            return 0;        }        if ($a['priority'] > $b['priority']) {            return -1;        }        return 1;    }}
 |