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
![]() |
|||
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
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.. ![]() |
|||
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 |