Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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