├── LICENSE ├── composer.json └── src ├── DI └── EventDispatcherExtension.php ├── Diagnostics ├── DebugDispatcher.php ├── EventTrace.php └── TracyDispatcher.php ├── Exceptions └── LogicalException.php ├── LazyListener.php └── Tracy ├── EventPanel.php └── templates ├── panel.phtml └── tab.phtml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Contributte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contributte/event-dispatcher", 3 | "description": "Best event dispatcher / event manager / event emitter for Nette Framework", 4 | "keywords": [ 5 | "nette", 6 | "symfony", 7 | "event", 8 | "dispatcher", 9 | "emitter" 10 | ], 11 | "type": "library", 12 | "license": "MIT", 13 | "homepage": "https://github.com/contributte/event-dispatcher", 14 | "authors": [ 15 | { 16 | "name": "Milan Felix Šulc", 17 | "homepage": "https://f3l1x.io" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=8.1", 22 | "nette/di": "^3.1.8", 23 | "symfony/event-dispatcher": "^6.4.3 || ^7.0.3" 24 | }, 25 | "require-dev": { 26 | "psr/log": "^2.0.0 || ^3.0.0", 27 | "tracy/tracy": "^2.10.5", 28 | "contributte/qa": "^0.4", 29 | "contributte/tester": "^0.3", 30 | "contributte/phpstan": "^0.1", 31 | "mockery/mockery": "^1.5.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Contributte\\EventDispatcher\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Tests\\": "tests" 41 | } 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true, 45 | "config": { 46 | "sort-packages": true, 47 | "allow-plugins": { 48 | "dealerdirect/phpcodesniffer-composer-installer": true 49 | } 50 | }, 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "0.10.x-dev" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/DI/EventDispatcherExtension.php: -------------------------------------------------------------------------------- 1 | Expect::bool(true), 33 | 'autoload' => Expect::bool(true), 34 | 'debug' => Expect::bool(false), 35 | 'loggers' => Expect::arrayOf(Expect::type(Statement::class)), 36 | ]); 37 | } 38 | 39 | public function loadConfiguration(): void 40 | { 41 | $builder = $this->getContainerBuilder(); 42 | $config = $this->getConfig(); 43 | 44 | // Original dispatcher 45 | $outerDispatcher = $dispatcherDef = $builder->addDefinition($this->prefix('dispatcher')) 46 | ->setType(EventDispatcherInterface::class) 47 | ->setFactory(EventDispatcher::class) 48 | ->setAutowired(false); 49 | 50 | // Dispatcher for logging 51 | if ($config->loggers !== []) { 52 | $loggingDispatcherDef = $builder->addDefinition($this->prefix('dispatcher.logging')) 53 | ->setFactory(DebugDispatcher::class, [$outerDispatcher]) 54 | ->setAutowired(false); 55 | $outerDispatcher = $loggingDispatcherDef; 56 | } 57 | 58 | // Dispatcher for Tracy bar 59 | if ($config->debug === true) { 60 | $tracyDispatcherDef = $builder->addDefinition($this->prefix('dispatcher.tracy')) 61 | ->setType(EventDispatcherInterface::class) 62 | ->setFactory(TracyDispatcher::class, [$outerDispatcher]) 63 | ->setAutowired(false); 64 | $outerDispatcher = $tracyDispatcherDef; 65 | } 66 | 67 | // Only outer dispatcher should be autowired 68 | $outerDispatcher->setAutowired(); 69 | } 70 | 71 | public function beforeCompile(): void 72 | { 73 | $config = $this->getConfig(); 74 | 75 | if ($config->autoload === true) { 76 | if ($config->lazy === true) { 77 | $this->doBeforeCompileLaziness(); 78 | } else { 79 | $this->doBeforeCompile(); 80 | } 81 | } 82 | } 83 | 84 | public function afterCompile(ClassType $class): void 85 | { 86 | $config = $this->getConfig(); 87 | $builder = $this->getContainerBuilder(); 88 | $initialization = $this->getInitialization(); 89 | 90 | if ($config->debug) { 91 | $initialization->addBody( 92 | // @phpstan-ignore-next-line 93 | $builder->formatPhp('?->addPanel(?);', [ 94 | $builder->getDefinitionByType(Bar::class), 95 | new Statement(EventPanel::class, [$builder->getDefinition($this->prefix('dispatcher.tracy'))]), 96 | ]) 97 | ); 98 | } 99 | } 100 | 101 | /** 102 | * Collect listeners and subscribers 103 | */ 104 | private function doBeforeCompile(): void 105 | { 106 | $builder = $this->getContainerBuilder(); 107 | $dispatcher = $builder->getDefinition($this->prefix('dispatcher')); 108 | assert($dispatcher instanceof ServiceDefinition); 109 | 110 | $subscribers = $builder->findByType(EventSubscriberInterface::class); 111 | foreach ($subscribers as $subscriber) { 112 | $dispatcher->addSetup('addSubscriber', [$subscriber]); 113 | } 114 | } 115 | 116 | /** 117 | * Collect listeners and subscribers in lazy-way 118 | */ 119 | private function doBeforeCompileLaziness(): void 120 | { 121 | $builder = $this->getContainerBuilder(); 122 | $dispatcher = $builder->getDefinition($this->prefix('dispatcher')); 123 | assert($dispatcher instanceof ServiceDefinition); 124 | 125 | $subscribers = $builder->findByType(EventSubscriberInterface::class); 126 | foreach ($subscribers as $serviceName => $subscriber) { 127 | assert($subscriber instanceof ServiceDefinition); 128 | $events = call_user_func([$subscriber->getEntity(), 'getSubscribedEvents']); // @phpstan-ignore-line 129 | assert(is_array($events)); 130 | 131 | foreach ($events as $event => $params) { 132 | if (is_string($params)) { // ['eventName' => 'methodName'] 133 | if (!method_exists((string) $subscriber->getType(), $params)) { 134 | throw new ServiceCreationException(sprintf('Event listener %s does not have callable method %s', $subscriber->getType(), $params)); 135 | } 136 | 137 | $dispatcher->addSetup('addListener', [ 138 | 'eventName' => $event, 139 | 'listener' => new Statement(LazyListener::class, [$serviceName, $params, $builder->getDefinitionByType(Container::class)]), 140 | 'priority' => 0, 141 | ]); 142 | } elseif (is_string($params[0])) { // ['eventName' => ['methodName', $priority]] 143 | if (!method_exists((string) $subscriber->getType(), $params[0])) { 144 | throw new ServiceCreationException(sprintf('Event listener %s does not have callable method %s', $subscriber->getType(), $params[0])); 145 | } 146 | 147 | $dispatcher->addSetup('addListener', [ 148 | 'eventName' => $event, 149 | 'listener' => new Statement(LazyListener::class, [$serviceName, $params[0], $builder->getDefinitionByType(Container::class)]), 150 | 'priority' => $params[1] ?? 0, 151 | ]); 152 | } elseif (is_array($params[0])) { // ['eventName' => [['methodName1', $priority], ['methodName2']]] 153 | foreach ($params as $listener) { 154 | if (!method_exists((string) $subscriber->getType(), $listener[0])) { 155 | throw new ServiceCreationException(sprintf('Event listener %s does not have callable method %s', $subscriber->getType(), $listener[0])); 156 | } 157 | 158 | $dispatcher->addSetup('addListener', [ 159 | 'eventName' => $event, 160 | 'listener' => new Statement(LazyListener::class, [$serviceName, $listener[0], $builder->getDefinitionByType(Container::class)]), 161 | 'priority' => $listener[1] ?? 0, 162 | ]); 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /src/Diagnostics/DebugDispatcher.php: -------------------------------------------------------------------------------- 1 | original = $original; 20 | } 21 | 22 | public function addLogger(LoggerInterface $logger): void 23 | { 24 | $this->loggers[] = $logger; 25 | } 26 | 27 | /** 28 | * @param LoggerInterface[] $loggers 29 | */ 30 | public function setLoggers(array $loggers = []): void 31 | { 32 | $this->loggers = $loggers; 33 | } 34 | 35 | /** 36 | * @return LoggerInterface[] 37 | */ 38 | public function getLoggers(): array 39 | { 40 | return $this->loggers; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function addListener(string $eventName, callable $listener, int $priority = 0): void 47 | { 48 | $this->original->addListener($eventName, $listener, $priority); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function addSubscriber(EventSubscriberInterface $subscriber): void 55 | { 56 | $this->original->addSubscriber($subscriber); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function removeListener(string $eventName, callable $listener): void 63 | { 64 | $this->original->removeListener($eventName, $listener); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function removeSubscriber(EventSubscriberInterface $subscriber): void 71 | { 72 | $this->original->removeSubscriber($subscriber); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function getListeners(?string $eventName = null): array 79 | { 80 | return $this->original->getListeners($eventName); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function getListenerPriority(string $eventName, callable $listener): ?int 87 | { 88 | return $this->original->getListenerPriority($eventName, $listener); 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function hasListeners(?string $eventName = null): bool 95 | { 96 | return $this->original->hasListeners($eventName); 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function dispatch(object $event, ?string $eventName = null): object 103 | { 104 | $trace = new EventTrace($event, $eventName); 105 | 106 | // Iterate over all loggers 107 | foreach ($this->loggers as $logger) { 108 | $logger->debug(sprintf('EventDispatcher@%s: event started', $trace->name), ['event' => $trace]); 109 | } 110 | 111 | // Start timer 112 | $start = microtime(true); 113 | 114 | // Dispatch event 115 | $return = $this->original->dispatch($event, $eventName); 116 | 117 | // If event was handled, mark it 118 | if ($this->original->hasListeners($trace->name)) { 119 | $trace->handled = true; 120 | } 121 | 122 | // Calculate duration 123 | $trace->duration = microtime(true) - $start; 124 | 125 | foreach ($this->loggers as $logger) { 126 | $logger->debug(sprintf('EventDispatcher@%s: event dispatched', $trace->name), ['event' => $trace]); 127 | } 128 | 129 | return $return; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/Diagnostics/EventTrace.php: -------------------------------------------------------------------------------- 1 | event = $event; 19 | $this->name = $eventName ?? $event::class; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Diagnostics/TracyDispatcher.php: -------------------------------------------------------------------------------- 1 | original = $original; 19 | } 20 | 21 | /** 22 | * @return EventTrace[] 23 | */ 24 | public function getEvents(): array 25 | { 26 | return $this->events; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function addListener(string $eventName, callable $listener, int $priority = 0): void 33 | { 34 | $this->original->addListener($eventName, $listener, $priority); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function addSubscriber(EventSubscriberInterface $subscriber): void 41 | { 42 | $this->original->addSubscriber($subscriber); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function removeListener(string $eventName, callable $listener): void 49 | { 50 | $this->original->removeListener($eventName, $listener); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function removeSubscriber(EventSubscriberInterface $subscriber): void 57 | { 58 | $this->original->removeSubscriber($subscriber); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function getListeners(?string $eventName = null): array 65 | { 66 | return $this->original->getListeners($eventName); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function getListenerPriority(string $eventName, callable $listener): ?int 73 | { 74 | return $this->original->getListenerPriority($eventName, $listener); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function hasListeners(?string $eventName = null): bool 81 | { 82 | return $this->original->hasListeners($eventName); 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function dispatch(object $event, ?string $eventName = null): object 89 | { 90 | $trace = new EventTrace($event, $eventName); 91 | 92 | // Store trace 93 | $this->events[] = $trace; 94 | 95 | // Start timer 96 | $start = microtime(true); 97 | 98 | // Dispatch event 99 | $return = $this->original->dispatch($event, $eventName); 100 | 101 | // If event was handled, mark it 102 | if ($this->original->hasListeners($trace->name)) { 103 | $trace->handled = true; 104 | } 105 | 106 | // Calculate duration 107 | $trace->duration = microtime(true) - $start; 108 | 109 | return $return; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/Exceptions/LogicalException.php: -------------------------------------------------------------------------------- 1 | service ?? $this) . '::' . $this->methodName; 23 | } 24 | 25 | public function __invoke(): mixed 26 | { 27 | if ($this->service === null) { 28 | $this->service = $this->container->getService($this->serviceName); 29 | } 30 | 31 | return $this->service->{$this->methodName}(...func_get_args()); // @phpstan-ignore-line 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Tracy/EventPanel.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function getTab(): string 22 | { 23 | $totalCount = count($this->dispatcher->getEvents()); // @phpcs:ignore 24 | $handledCount = $this->handledCount(); // @phpcs:ignore 25 | $totalTime = $this->countTotalTime(); // @phpcs:ignore 26 | $totalTime = number_format($totalTime * 1000, 1, '.', ' ') . ' ms'; // @phpcs:ignore 27 | 28 | ob_start(); 29 | require __DIR__ . '/templates/tab.phtml'; 30 | 31 | return (string) ob_get_clean(); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getPanel(): string 38 | { 39 | $handledCount = $this->handledCount(); // @phpcs:ignore 40 | $totalTime = $this->countTotalTime(); // @phpcs:ignore 41 | $events = $this->dispatcher->getEvents(); // @phpcs:ignore 42 | $listeners = $this->dispatcher->getListeners(); 43 | ksort($listeners); 44 | ob_start(); 45 | require __DIR__ . '/templates/panel.phtml'; 46 | 47 | return (string) ob_get_clean(); 48 | } 49 | 50 | private function countTotalTime(): float 51 | { 52 | $totalTime = 0; 53 | foreach ($this->dispatcher->getEvents() as $event) { 54 | $totalTime += $event->duration; 55 | } 56 | 57 | return $totalTime; 58 | } 59 | 60 | private function handledCount(): int 61 | { 62 | $handled = 0; 63 | foreach ($this->dispatcher->getEvents() as $event) { 64 | $handled += $event->handled ? 1 : 0; 65 | } 66 | 67 | return $handled; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/Tracy/templates/panel.phtml: -------------------------------------------------------------------------------- 1 | $listeners */ 4 | /** @var int $handledCount */ 5 | 6 | /** @var float $totalTime */ 7 | 8 | use Contributte\EventDispatcher\Diagnostics\EventInfo; 9 | use Tracy\Dumper; 10 | ?> 11 | 20 | 21 |

No events handled

22 | 23 |

Events: 24 | , 25 | time: 26 | ms 27 |

28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 49 | 50 | 51 |
Time msHandledEvent
43 | duration * 1000); ?> 44 | handled ? 'yes' : 'no' ?> 47 | event, [Dumper::COLLAPSE => true]); ?> 48 |
52 | 53 | 54 |
55 |

Listeners

56 | 57 | 58 | 59 | 60 | 61 | 62 | $listen): ?> 63 | $handler): ?> 64 | 65 | 66 | 69 | 70 | 81 | 82 | 83 | 84 |
EventListeners
67 | 68 | 71 | toString(); 76 | } else { 77 | echo Dumper::toHtml($handler); 78 | } 79 | ?> 80 |
85 | 86 |
87 | true]); ?> 88 |
89 |
90 |
91 |
92 | -------------------------------------------------------------------------------- /src/Tracy/templates/tab.phtml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | of / 12 | 13 | --------------------------------------------------------------------------------