biurad /
php-events-bus
| 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
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
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 |