TraceableEventDispatcher::getNotCalledListeners()   B
last analyzed

Complexity

Conditions 9
Paths 12

Size

Total Lines 41
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 9

Importance

Changes 0
Metric Value
cc 9
eloc 21
nc 12
nop 0
dl 0
loc 41
ccs 21
cts 21
cp 1
crap 9
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.2 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\Events;
19
20
use Psr\EventDispatcher\StoppableEventInterface;
21
use Psr\Log\LoggerInterface;
22
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
23
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
24
25
class TraceableEventDispatcher implements EventDispatcherInterface
26
{
27
    /** @var null|LoggerInterface */
28
    protected $logger;
29
30
    /** @var EventDispatcherInterface */
31
    private $dispatcher;
32
33
    /** @var array<int,array<string,string>> */
34
    private $eventsLog;
35
36
    /** @var null|SplObjectStorage */
0 ignored issues
show
Bug introduced by
The type Biurad\Events\SplObjectStorage was not found. Did you mean SplObjectStorage? If so, make sure to prefix the type with \.
Loading history...
37
    private $callStack;
38
39
    /** @var array<string,array<int,mixed>> */
40
    private $wrappedListeners;
41
42
    /** @var string[] */
43
    private $orphanedEvents;
44
45 25
    public function __construct(EventDispatcherInterface $dispatcher, LoggerInterface $logger = null)
46
    {
47 25
        $this->dispatcher       = $dispatcher;
48
49 25
        $this->logger           = $logger;
50 25
        $this->wrappedListeners = [];
51 25
        $this->orphanedEvents   = [];
52 25
        $this->eventsLog        = [];
53 25
    }
54
55
    /**
56
     * Proxies all method calls to the original event dispatcher.
57
     *
58
     * @param string                  $method    The method name
59
     * @param array<int|string,mixed> $arguments The method arguments
60
     *
61
     * @return mixed
62
     */
63 3
    public function __call(string $method, array $arguments)
64
    {
65 3
        return $this->dispatcher->{$method}(...$arguments);
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 20
    public function addListener(string $eventName, $listener, int $priority = 0): void
72
    {
73 20
        $this->dispatcher->addListener($eventName, $listener, $priority);
74 20
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79 1
    public function addSubscriber(EventSubscriberInterface $subscriber): void
80
    {
81 1
        $this->dispatcher->addSubscriber($subscriber);
82 1
    }
83
84
    /**
85
     * {@inheritdoc}
86
     *
87
     * @param mixed $listener
88
     *
89
     * @return mixed
90
     */
91 2
    public function removeListener(string $eventName, $listener)
92
    {
93 2
        if (isset($this->wrappedListeners[$eventName])) {
94 1
            foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) {
95 1
                if ($wrappedListener->getWrappedListener() === $listener) {
96 1
                    $listener = $wrappedListener;
97 1
                    unset($this->wrappedListeners[$eventName][$index]);
98
99 1
                    break;
100
                }
101
            }
102
        }
103
104 2
        return $this->dispatcher->removeListener($eventName, $listener);
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     *
110
     * @return mixed
111
     */
112 1
    public function removeSubscriber(EventSubscriberInterface $subscriber)
113
    {
114 1
        return $this->dispatcher->removeSubscriber($subscriber);
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     *
120
     * @return array<string,mixed>
121
     */
122 5
    public function getListeners(string $eventName = null)
123
    {
124 5
        return $this->dispatcher->getListeners($eventName);
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     *
130
     * @param mixed $listener
131
     */
132 17
    public function getListenerPriority(string $eventName, $listener)
133
    {
134
        // we might have wrapped listeners for the event (if called while dispatching)
135
        // in that case get the priority by wrapper
136 17
        if (isset($this->wrappedListeners[$eventName])) {
137 15
            foreach ($this->wrappedListeners[$eventName] as $wrappedListener) {
138 15
                if ($wrappedListener->getWrappedListener() === $listener) {
139 15
                    return $this->dispatcher->getListenerPriority($eventName, $wrappedListener);
140
                }
141
            }
142
        }
143
144 17
        return $this->dispatcher->getListenerPriority($eventName, $listener);
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 1
    public function hasListeners(string $eventName = null)
151
    {
152 1
        return $this->dispatcher->hasListeners($eventName);
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 18
    public function dispatch(object $event, string $eventName = null): object
159
    {
160 18
        $eventName = $eventName ?? \get_class($event);
161
162 18
        if (null === $this->callStack) {
163 18
            $this->callStack = new \SplObjectStorage();
0 ignored issues
show
Documentation Bug introduced by
It seems like new SplObjectStorage() of type SplObjectStorage is incompatible with the declared type Biurad\Events\SplObjectStorage|null of property $callStack.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
164
        }
165
166 18
        if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
167 1
            $this->logger->debug(
168 1
                \sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName)
169
            );
170
        }
171
172 18
        $timerStart = (float) \microtime(true);
173 18
        $this->preProcess($eventName);
174
175
        try {
176 18
            $this->dispatcher->dispatch($event, $eventName);
177 18
        } finally {
178 18
            $this->postProcess($eventName);
179
180
            // Enable Profiling
181 18
            $this->setEventsLog($eventName, $timerStart);
182
        }
183
184 18
        return $event;
185
    }
186
187
    /**
188
     * @return array<int,array<string,mixed>>
189
     */
190 4
    public function getCalledListeners()
191
    {
192 4
        if (null === $this->callStack) {
193 2
            return [];
194
        }
195
196 3
        $called = [];
197
198
        /** @var WrappedListener $listener */
199 3
        foreach ($this->callStack as $listener) {
200 3
            $called[] = $listener->getInfo(\current($this->callStack->getInfo()));
201
        }
202
203 3
        return $called;
204
    }
205
206
    /**
207
     * @return array<int,array<string,mixed>>
208
     */
209 3
    public function getNotCalledListeners(): array
210
    {
211
        try {
212 3
            $allListeners = $this->getListeners();
213 1
        } catch (\Exception $e) {
214 1
            if (null !== $this->logger) {
215 1
                $this->logger->info(
216 1
                    'An exception was thrown while getting the uncalled listeners.',
217 1
                    ['exception' => $e]
218
                );
219
            }
220
221
            // unable to retrieve the uncalled listeners
222 1
            return [];
223
        }
224
225 2
        $calledListeners = [];
226
227 2
        if (null !== $this->callStack) {
228
            /** @var WrappedListener $calledListener */
229 1
            foreach ($this->callStack as $calledListener) {
230 1
                $calledListeners[] = $calledListener->getWrappedListener();
231
            }
232
        }
233
234 2
        $notCalled = [];
235
236 2
        foreach ($allListeners as $eventName => $listeners) {
237 2
            foreach ($listeners as $listener) {
238 2
                if (!\in_array($listener, $calledListeners, true)) {
239 2
                    if (!$listener instanceof WrappedListener) {
240 2
                        $listener = new WrappedListener($listener, null, $this);
241
                    }
242 2
                    $notCalled[] = $listener->getInfo($eventName);
243
                }
244
            }
245
        }
246
247 2
        \uasort($notCalled, [$this, 'sortNotCalledListeners']);
248
249 2
        return $notCalled;
250
    }
251
252
    /**
253
     * @return string[]
254
     */
255 5
    public function getOrphanedEvents(): array
256
    {
257 5
        if (empty($this->orphanedEvents)) {
258 4
            return [];
259
        }
260
261 2
        return \array_values($this->orphanedEvents);
262
    }
263
264 3
    public function reset(): void
265
    {
266 3
        $this->callStack      = null;
267 3
        $this->orphanedEvents = [];
268 3
    }
269
270
    /**
271
     * Getter for the performance log records.
272
     *
273
     * @return array<int,array<string,string>>
274
     */
275 1
    public function getEventsLogs(): array
276
    {
277 1
        return $this->eventsLog;
278
    }
279
280
    /**
281
     * @return EventDispatcherInterface
282
     */
283 3
    public function getDispatcher(): EventDispatcherInterface
284
    {
285 3
        return $this->dispatcher;
286
    }
287
288
    /**
289
     * Setter for the performance log records.
290
     *
291
     * @param string $eventName
292
     * @param float  $timerStart
293
     */
294 18
    private function setEventsLog(string $eventName, float $timerStart): void
295
    {
296 18
        $this->eventsLog[] = [
297 18
            'event'    => $eventName,
298 18
            'duration' => \number_format((\microtime(true) - $timerStart) * 1000, 2) . 'ms',
299
        ];
300 18
    }
301
302 18
    private function preProcess(string $eventName): void
303
    {
304 18
        if (!$this->dispatcher->hasListeners($eventName)) {
305 2
            $this->orphanedEvents[] = $eventName;
306
307 2
            return;
308
        }
309
310 16
        foreach ($this->dispatcher->getListeners($eventName) as $listener) {
311 16
            $priority                             = $this->getListenerPriority($eventName, $listener);
312 16
            $wrappedListener                      = new WrappedListener(
313 16
                $listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener,
314 16
                null,
315
                $this
316
            );
317 16
            $this->wrappedListeners[$eventName][] = $wrappedListener;
318 16
            $this->dispatcher->removeListener($eventName, $listener);
319 16
            $this->dispatcher->addListener($eventName, $wrappedListener, $priority ?? 0);
320
321 16
            if (null !== $this->callStack) {
322 16
                $this->callStack->attach($wrappedListener, \compact('eventName'));
323
            }
324
        }
325 16
    }
326
327 18
    private function postProcess(string $eventName): void
328
    {
329 18
        unset($this->wrappedListeners[$eventName]);
330 18
        $skipped = false;
331
332 18
        foreach ($this->dispatcher->getListeners($eventName) as $listener) {
333 16
            if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch.
334 1
                continue;
335
            }
336
337
            // Unwrap listener
338 16
            $priority = $this->getListenerPriority($eventName, $listener);
339 16
            $this->dispatcher->removeListener($eventName, $listener);
340 16
            $this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority ?? 0);
341
342 16
            $context = null !== $this->logger ? ['event' => $eventName, 'listener' => $listener->getPretty()] : [];
343
344 16
            if ($listener->wasCalled()) {
345 15
                if (null !== $this->logger) {
346 15
                    $this->logger->debug('Notified event "{event}" to listener "{listener}".', $context);
347
                }
348 2
            } elseif (null !== $this->callStack) {
349 2
                $this->callStack->detach($listener);
350
            }
351
352 16
            if (null !== $this->logger && $skipped) {
353 1
                $this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context);
354
            }
355
356 16
            if ($listener->stoppedPropagation()) {
357 1
                if (null !== $this->logger) {
358 1
                    $this->logger->debug(
359 1
                        'Listener "{listener}" stopped propagation of the event "{event}".',
360
                        $context
361
                    );
362
                }
363
364 1
                $skipped = true;
365
            }
366
        }
367 18
    }
368
369
    /**
370
     * @codeCoverageIgnore
371
     *
372
     * @param array<string,mixed> $a
373
     * @param array<string,mixed> $b
374
     */
375
    private function sortNotCalledListeners(array $a, array $b): int
376
    {
377
        if (0 !== $cmp = \strcmp($a['event'], $b['event'])) {
378
            return $cmp;
379
        }
380
381
        if (\is_int($a['priority']) && !\is_int($b['priority'])) {
382
            return 1;
383
        }
384
385
        if (!\is_int($a['priority']) && \is_int($b['priority'])) {
386
            return -1;
387
        }
388
389
        if ($a['priority'] === $b['priority']) {
390
            return 0;
391
        }
392
393
        if ($a['priority'] > $b['priority']) {
394
            return -1;
395
        }
396
397
        return 1;
398
    }
399
}
400