├── 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 | = $handledCount ?>,
25 | time:
26 | = $totalTime ? sprintf('%0.3f', $totalTime * 1000) : ''; ?> ms
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Time ms |
36 | Handled |
37 | Event |
38 |
39 |
40 |
41 |
42 |
43 | = sprintf('%0.2f', $e->duration * 1000); ?>
44 | |
45 | = $e->handled ? 'yes' : 'no' ?> |
46 |
47 | = Dumper::toHtml($e->event, [Dumper::COLLAPSE => true]); ?>
48 | |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
Listeners
56 |
57 |
58 | Event |
59 | Listeners |
60 |
61 |
62 | $listen): ?>
63 | $handler): ?>
64 |
65 |
66 |
67 | = $eventName ?>
68 | |
69 |
70 |
71 | toString();
76 | } else {
77 | echo Dumper::toHtml($handler);
78 | }
79 | ?>
80 | |
81 |
82 |
83 |
84 |
85 |
86 |
87 | = Dumper::toHtml($listeners, [Dumper::COLLAPSE => true]); ?>
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/Tracy/templates/tab.phtml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 | = $handledCount ?> of = $totalCount ?> / = $totalTime ?>
12 |
13 |
--------------------------------------------------------------------------------