├── LICENSE ├── README.md ├── composer.json └── src ├── EventManager.php ├── Exception.php └── ExceptionStop.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 JBZoo Content Construction Kit (CCK) 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JBZoo / Event 2 | 3 | [![CI](https://github.com/JBZoo/Event/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/JBZoo/Event/actions/workflows/main.yml?query=branch%3Amaster) [![Coverage Status](https://coveralls.io/repos/github/JBZoo/Event/badge.svg?branch=master)](https://coveralls.io/github/JBZoo/Event?branch=master) [![Psalm Coverage](https://shepherd.dev/github/JBZoo/Event/coverage.svg)](https://shepherd.dev/github/JBZoo/Event) [![Psalm Level](https://shepherd.dev/github/JBZoo/Event/level.svg)](https://shepherd.dev/github/JBZoo/Event) [![CodeFactor](https://www.codefactor.io/repository/github/jbzoo/event/badge)](https://www.codefactor.io/repository/github/jbzoo/event/issues) 4 | [![Stable Version](https://poser.pugx.org/jbzoo/event/version)](https://packagist.org/packages/jbzoo/event/) [![Total Downloads](https://poser.pugx.org/jbzoo/event/downloads)](https://packagist.org/packages/jbzoo/event/stats) [![Dependents](https://poser.pugx.org/jbzoo/event/dependents)](https://packagist.org/packages/jbzoo/event/dependents?order_by=downloads) [![GitHub License](https://img.shields.io/github/license/jbzoo/event)](https://github.com/JBZoo/Event/blob/master/LICENSE) 5 | 6 | 7 | The EventEmitter is a simple pattern that allows you to create an object that emits events, and allow you to listen to those events. 8 | 9 | ### Install 10 | ```sh 11 | composer require jbzoo/event 12 | ``` 13 | 14 | 15 | ### Simple example 16 | ```php 17 | use JBZoo\Event\EventManager; 18 | 19 | $eManager = new EventManager(); 20 | 21 | // Simple 22 | $eManager->on('create', function () { 23 | echo "Something action"; 24 | }); 25 | 26 | // Just do it! 27 | $eManager->trigger('create'); 28 | ``` 29 | 30 | 31 | ### Set priority 32 | By supplying a priority, you are ensured that subscribers handle in a specific order. The default priority is EventManager::MID. 33 | Anything below that will be triggered earlier, anything higher later. 34 | If there are two subscribers with the same priority, they will execute in an undefined, but deterministic order. 35 | ```php 36 | // Run it first 37 | $eManager->on('create', function () { 38 | echo "Something high priority action"; 39 | }, EventManager::HIGH); 40 | 41 | // Run it latest 42 | $eManager->on('create', function () { 43 | echo "Something another action"; 44 | }, EventManager::LOW); 45 | 46 | // Custom index 47 | $eManager->on('create', function () { 48 | echo "Something action"; 49 | }, 42); 50 | 51 | // Don't care... 52 | $eManager->on('create', function () { 53 | echo "Something action"; 54 | }); 55 | ``` 56 | 57 | ### Types of Callback 58 | All default PHP callbacks are supported, so closures are not required. 59 | ```php 60 | $eManager->on('create', function(){ /* ... */ }); // Custom function 61 | $eManager->on('create', 'myFunction'); // Custom function name 62 | $eManager->on('create', ['myClass', 'myMethod']); // Static function 63 | $eManager->on('create', [$object, 'Method']); // Method of instance 64 | ``` 65 | 66 | 67 | ### Cancel queue of events 68 | ```php 69 | use JBZoo\Event\ExceptionStop; 70 | 71 | $eManager->on('create', function () { 72 | throw new ExceptionStop('Some reason'); // Special exception for JBZoo/Event 73 | }); 74 | 75 | $eManager->trigger('create'); // return 'Some reason' or TRUE if all events done 76 | ``` 77 | 78 | 79 | ### Passing arguments 80 | Arguments can be passed as an array. 81 | ```php 82 | $eManager->on('create', function ($entityId) { 83 | echo "An entity with id ", $entityId, " just got created.\n"; 84 | }); 85 | $entityId = 5; 86 | $eManager->trigger('create', [$entityId]); 87 | ``` 88 | 89 | Because you cannot really do anything with the return value of a listener, you can pass arguments by reference to communicate between listeners and back to the emitter. 90 | ```php 91 | $eManager->on('create', function ($entityId, &$warnings) { 92 | echo "An entity with id ", $entityId, " just got created.\n"; 93 | $warnings[] = "Something bad may or may not have happened.\n"; 94 | }); 95 | $warnings = []; 96 | $eManager->trigger('create', [$entityId, &$warnings]); 97 | ``` 98 | 99 | ### Namespaces 100 | ```php 101 | $eManager->on('item.*', function () { 102 | // item.init 103 | // item.save 104 | echo "Any actions with item"; 105 | }); 106 | 107 | $eManager->on('*.init', function () { 108 | // tag.init 109 | // item.init 110 | echo "Init any entity"; 111 | }); 112 | 113 | $eManager->on('*.save', function () { 114 | // tag.save 115 | // item.save 116 | echo "Saving any entity in system"; 117 | }); 118 | 119 | $eManager->on('*.save.after', function () { 120 | // tag.save.after 121 | // item.save.after 122 | echo "Any entity on after save"; 123 | }); 124 | 125 | $eManager->trigger('tag.init'); 126 | $eManager->trigger('tag.save.before'); 127 | $eManager->trigger('tag.save'); 128 | $eManager->trigger('tag.save.after'); 129 | 130 | $eManager->trigger('item.init'); 131 | $eManager->trigger('item.save.before'); 132 | $eManager->trigger('item.save'); 133 | $eManager->trigger('item.save.after'); 134 | ``` 135 | 136 | 137 | ## Summary benchmark info (execution time) PHP v7.4 138 | All benchmark tests are executing without xdebug and with a huge random array and 100.000 iterations. 139 | 140 | Benchmark tests based on the tool [phpbench/phpbench](https://github.com/phpbench/phpbench). See details [here](tests/phpbench). 141 | 142 | Please, pay attention - `1μs = 1/1.000.000 of second!` 143 | 144 | **benchmark: ManyCallbacks** 145 | 146 | subject | groups | its | revs | mean | stdev | rstdev | mem_real | diff 147 | --- | --- | --- | --- | --- | --- | --- | --- | --- 148 | benchOneUndefined | undefined | 10 | 100000 | 0.65μs | 0.01μs | 1.00% | 6,291,456b | 1.00x 149 | benchOneWithStarBegin | *.bar | 10 | 100000 | 0.67μs | 0.01μs | 1.44% | 6,291,456b | 1.04x 150 | benchOneWithAllStars | \*.\* | 10 | 100000 | 0.68μs | 0.03μs | 4.18% | 6,291,456b | 1.04x 151 | benchOneWithStarEnd | foo.* | 10 | 100000 | 0.68μs | 0.01μs | 1.24% | 6,291,456b | 1.04x 152 | benchOneNested | foo.bar | 10 | 100000 | 43.23μs | 0.46μs | 1.07% | 6,291,456b | 66.56x 153 | benchOneSimple | foo | 10 | 100000 | 45.07μs | 2.63μs | 5.83% | 6,291,456b | 69.39x 154 | 155 | **benchmark: ManyCallbacksWithPriority** 156 | 157 | subject | groups | its | revs | mean | stdev | rstdev | mem_real | diff 158 | --- | --- | --- | --- | --- | --- | --- | --- | --- 159 | benchOneUndefined | undefined | 10 | 100000 | 0.65μs | 0.01μs | 1.35% | 6,291,456b | 1.00x 160 | benchOneNestedStarAll | \*.\* | 10 | 100000 | 0.67μs | 0.01μs | 1.34% | 6,291,456b | 1.03x 161 | benchOneWithStarBegin | *.bar | 10 | 100000 | 0.67μs | 0.01μs | 1.10% | 6,291,456b | 1.04x 162 | benchOneWithStarEnd | foo.* | 10 | 100000 | 0.68μs | 0.01μs | 1.13% | 6,291,456b | 1.05x 163 | benchOneSimple | foo | 10 | 100000 | 4.54μs | 0.02μs | 0.35% | 6,291,456b | 7.03x 164 | benchOneNested | foo.bar | 10 | 100000 | 4.58μs | 0.04μs | 0.81% | 6,291,456b | 7.10x 165 | 166 | **benchmark: OneCallback** 167 | 168 | subject | groups | its | revs | mean | stdev | rstdev | mem_real | diff 169 | --- | --- | --- | --- | --- | --- | --- | --- | --- 170 | benchOneWithStarBegin | *.bar | 10 | 100000 | 0.69μs | 0.03μs | 4.00% | 6,291,456b | 1.00x 171 | benchOneWithStarEnd | foo.* | 10 | 100000 | 0.70μs | 0.03μs | 4.22% | 6,291,456b | 1.00x 172 | benchOneNestedStarAll | \*.\* | 10 | 100000 | 0.70μs | 0.04μs | 6.02% | 6,291,456b | 1.01x 173 | benchOneUndefined | undefined | 10 | 100000 | 0.71μs | 0.05μs | 7.44% | 6,291,456b | 1.02x 174 | benchOneSimple | foo | 10 | 100000 | 1.18μs | 0.03μs | 2.27% | 6,291,456b | 1.70x 175 | benchOneNested | foo.bar | 10 | 100000 | 1.25μs | 0.03μs | 2.46% | 6,291,456b | 1.81x 176 | 177 | **benchmark: Random** 178 | 179 | subject | groups | its | revs | mean | stdev | rstdev | mem_real | diff 180 | --- | --- | --- | --- | --- | --- | --- | --- | --- 181 | benchOneSimple | random.*.triggers | 10 | 100000 | 4.29μs | 0.33μs | 7.69% | 6,291,456b | 1.00x 182 | 183 | 184 | ## Unit tests and check code style 185 | ```sh 186 | make update 187 | make test-all 188 | ``` 189 | 190 | 191 | ## License 192 | 193 | MIT 194 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "jbzoo/event", 3 | "type" : "library", 4 | "description" : "Library for event-based development", 5 | "license" : "MIT", 6 | "keywords" : [ 7 | "JBZoo", 8 | "events", 9 | "event-manager", 10 | "eventmanager", 11 | "listener", 12 | "hook", 13 | "signal", 14 | "observer", 15 | "emit", 16 | "emitter" 17 | ], 18 | 19 | "minimum-stability" : "dev", 20 | "prefer-stable" : true, 21 | 22 | "authors" : [ 23 | { 24 | "name" : "Denis Smetannikov", 25 | "email" : "admin@jbzoo.com", 26 | "role" : "lead" 27 | }, 28 | { 29 | "name" : "Evert Pot", 30 | "email" : "me@evertpot.com", 31 | "homepage" : "https://sabre.io/event/" 32 | } 33 | ], 34 | 35 | "require" : { 36 | "php" : "^8.1" 37 | }, 38 | 39 | "require-dev" : { 40 | "jbzoo/toolbox-dev" : "^7.1", 41 | "jbzoo/data" : "^7.1" 42 | }, 43 | 44 | "autoload" : { 45 | "psr-4" : {"JBZoo\\Event\\" : "src"} 46 | }, 47 | 48 | "autoload-dev" : { 49 | "psr-4" : {"JBZoo\\PHPUnit\\" : "tests"} 50 | }, 51 | 52 | "config" : { 53 | "optimize-autoloader" : true, 54 | "allow-plugins" : {"composer/package-versions-deprecated" : true} 55 | }, 56 | 57 | "extra" : { 58 | "branch-alias" : { 59 | "dev-master" : "7.x-dev" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/EventManager.php: -------------------------------------------------------------------------------- 1 | list)) { 45 | $this->list[$oneEventName] = []; 46 | } 47 | 48 | $this->list[$oneEventName][] = [$priority, $callback, $oneEventName]; 49 | } 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Subscribe to an event only once. 56 | */ 57 | public function once(string $eventName, callable $callback, int $priority = self::MID): self 58 | { 59 | $eventName = self::cleanEventName($eventName); 60 | 61 | $wrapper = null; 62 | 63 | /** @psalm-suppress MissingClosureReturnType */ 64 | $wrapper = function () use ($eventName, $callback, &$wrapper) { 65 | $this->removeListener($eventName, $wrapper); 66 | 67 | return $callback(...\func_get_args()); 68 | }; 69 | 70 | return $this->on($eventName, $wrapper, $priority); 71 | } 72 | 73 | /** 74 | * Emits an event. 75 | * 76 | * This method will return true if 0 or more listeners were successful 77 | * handled. false is returned if one of the events broke the event chain. 78 | * 79 | * If the continueCallBack is specified, this callback will be called every 80 | * time before the next event handler is called. 81 | * 82 | * If the continueCallback returns false, event propagation stops. This 83 | * allows you to use the eventEmitter as a means for listeners to implement 84 | * functionality in your application, and break the event loop as soon as 85 | * some condition is fulfilled. 86 | * 87 | * Note that returning false from an event subscriber breaks propagation 88 | * and returns false, but if the continue-callback stops propagation, this 89 | * is still considered a 'successful' operation and returns true. 90 | * 91 | * Lastly, if there are 5 event handlers for an event. The continueCallback 92 | * will be called at most 4 times. 93 | */ 94 | public function trigger(string $eventName, array $arguments = [], ?callable $continueCallback = null): int 95 | { 96 | $listeners = $this->getList($eventName); 97 | $arguments[] = self::cleanEventName($eventName); 98 | 99 | return self::callListenersWithCallback($listeners, $arguments, $continueCallback); 100 | } 101 | 102 | /** 103 | * Returns the list of listeners for an event. 104 | * The list is returned as an array, and the list of events are sorted by their priority. 105 | * @return callable[] 106 | */ 107 | public function getList(string $eventName): array 108 | { 109 | $eventName = self::cleanEventName($eventName); 110 | 111 | $result = []; 112 | $ePaths = \explode('.', $eventName); 113 | 114 | foreach ($this->list as $eName => $eData) { 115 | $eName = (string)$eName; 116 | if ($eName === $eventName) { 117 | /** @noinspection SlowArrayOperationsInLoopInspection */ 118 | $result = \array_merge($result, $eData); 119 | } elseif (\str_contains($eName, '*')) { 120 | $eNameParts = \explode('.', $eName); 121 | if (self::isContainPart($eNameParts, $ePaths)) { 122 | /** @noinspection SlowArrayOperationsInLoopInspection */ 123 | $result = \array_merge($result, $eData); 124 | } 125 | } 126 | } 127 | 128 | if (\count($result) > 0) { 129 | /** 130 | * @param array $item1 131 | * @param array $item2 132 | * @return int 133 | */ 134 | $sortFunc = static fn (array $item1, array $item2): int => (int)$item2[0] - (int)$item1[0]; 135 | \usort($result, $sortFunc); // Sorting by priority 136 | 137 | /** 138 | * @param array $item 139 | * @return callable 140 | */ 141 | $mapFunc = static fn (array $item): callable => $item[1]; 142 | 143 | return \array_map($mapFunc, $result); 144 | } 145 | 146 | return []; 147 | } 148 | 149 | /** 150 | * Removes a specific listener from an event. 151 | * If the listener could not be found, this method will return false. If it 152 | * was removed it will return true. 153 | */ 154 | public function removeListener(string $eventName, ?callable $listener = null): bool 155 | { 156 | $eventName = self::cleanEventName($eventName); 157 | 158 | if (!\array_key_exists($eventName, $this->list)) { 159 | return false; 160 | } 161 | 162 | foreach ($this->list[$eventName] as $index => $eventData) { 163 | if ($eventData[1] === $listener) { 164 | unset($this->list[$eventName][$index]); 165 | 166 | return true; 167 | } 168 | } 169 | 170 | return false; 171 | } 172 | 173 | /** 174 | * Removes all listeners. 175 | * If the eventName argument is specified, all listeners for that event are 176 | * removed. If it is not specified, every listener for every event is removed. 177 | */ 178 | public function removeListeners(?string $eventName = null): void 179 | { 180 | if ($eventName !== null) { 181 | $eventName = self::cleanEventName($eventName); 182 | } 183 | 184 | if ($eventName !== null && $eventName !== '') { 185 | unset($this->list[$eventName]); 186 | } else { 187 | $this->list = []; 188 | } 189 | } 190 | 191 | public function getSummeryInfo(): array 192 | { 193 | $result = []; 194 | 195 | foreach ($this->list as $eventName => $callbacks) { 196 | $result[$eventName] = \count($callbacks); 197 | } 198 | 199 | \ksort($result); 200 | 201 | return $result; 202 | } 203 | 204 | /** 205 | * Prepare event name before using. 206 | */ 207 | public static function cleanEventName(string $eventName): string 208 | { 209 | $eventName = \strtolower($eventName); 210 | $eventName = \str_replace('..', '.', $eventName); 211 | $eventName = \trim($eventName, '.'); 212 | $eventName = \trim($eventName); 213 | 214 | if ($eventName === '') { 215 | throw new Exception('Event name is empty!'); 216 | } 217 | 218 | return $eventName; 219 | } 220 | 221 | public static function setDefault(self $eManager): void 222 | { 223 | self::$defaultManager = $eManager; 224 | } 225 | 226 | public static function getDefault(): ?self 227 | { 228 | return self::$defaultManager; 229 | } 230 | 231 | /** 232 | * Call list of listeners with continue callback function. 233 | * @param callable[] $listeners 234 | */ 235 | private static function callListenersWithCallback( 236 | array $listeners, 237 | array $arguments = [], 238 | ?callable $continueCallback = null, 239 | ): int { 240 | $counter = \count($listeners); 241 | $execCounter = 0; 242 | 243 | foreach ($listeners as $listener) { 244 | $counter--; 245 | 246 | $result = self::callOneListener($listener, $arguments); 247 | if (!$result) { 248 | return $execCounter; 249 | } 250 | 251 | $execCounter++; 252 | 253 | if ($continueCallback !== null && $counter > 0 && !$continueCallback()) { 254 | break; 255 | } 256 | } 257 | 258 | return $execCounter; 259 | } 260 | 261 | /** 262 | * Call list of listeners. 263 | */ 264 | private static function callOneListener(callable $listener, array $arguments = []): bool 265 | { 266 | try { 267 | $listener(...$arguments); 268 | } catch (ExceptionStop) { 269 | return false; 270 | } 271 | 272 | return true; 273 | } 274 | 275 | /** 276 | * Check is one part name contain another one. 277 | */ 278 | private static function isContainPart(array $eNameParts, array $ePaths): bool 279 | { 280 | // Length of parts is equals 281 | if (\count($eNameParts) !== \count($ePaths)) { 282 | return false; 283 | } 284 | 285 | $isFound = true; 286 | 287 | foreach ($eNameParts as $pos => $eNamePart) { 288 | if ($eNamePart !== '*' && \array_key_exists($pos, $ePaths) && $ePaths[$pos] !== $eNamePart) { 289 | $isFound = false; 290 | break; 291 | } 292 | } 293 | 294 | return $isFound; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 |