diff --git a/README.md b/README.md index 77a7888..fe664a7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It is very fast event emitter and filters. ### Creating an Emitter ```php -$emitter = new PTS\Events\EventEmitter; +$emitter = new \PTS\Events\EventEmitter; ``` ### Adding Listeners @@ -31,13 +31,18 @@ With priority $emitter->on('user.created', $handler, 100); ``` -With extra arguments +With extra arguments for EventEmitterExtraArgs instance (EventEmitter instance skip extra args) ```php +$emitter = new \PTS\Events\EventEmitterExtraArgs; + $extra1 = 1; $extra2 = 'some'; -$emitter->on('log', function(string $log, int $extra1, string $extra2) { + +$handler = function(string $log, int $extra1, string $extra2) { // ... -}, 50, [$extra1, $extra2]); +}; + +$emitter->on('log', $handler, 50, [$extra1, $extra2]); $emitter->emit('log', ['some log']); ``` @@ -97,8 +102,7 @@ eventNames(): array; EventHandler must be `callable` ```php -use PTS\Events\Events; -$eventsBus = new EventEmitter; +$eventsBus = new \PTS\Events\EventEmitter; $eventsBus->on('some:event', function(){ ... }); $eventsBus->on('some:event', 'trim'); @@ -108,7 +112,7 @@ $eventsBus->once('some', $instanceWithInvokeMethod); ``` #### Order handlers -Listeners has priority. All listeners invoke by priority +Listeners have priority. All listeners invoke by priority ```php $events->on('post:title', 'trim', 10); // second @@ -133,9 +137,9 @@ $events->off('post:title'); #### StopPropagation ```php -$events->on('eventName', function(){ ... }); -$events->on('eventName', function(){ throw new StopPropagation; }); -$events->on('eventName', function(){ ... }); // it does not call +$events->on('eventName', function() { ... }); +$events->on('eventName', function() { throw new StopPropagation; }); +$events->on('eventName', function() { ... }); // it does not call ``` ## Filters @@ -155,20 +159,19 @@ eventNames(): array; Example ```php -use PTS\Events\Filters; -$filters = new Filters; +$filters = new \PTS\Events\Filters; $filters->on('post:title', 'trim'); $title = $filters->filter('post:title', ' Raw title '); // `Raw title` ``` - ### Inject EventEmitter / FilterEmitter 1. Event/Filter Bus. + ```php -use PTS\Events\EventBusTrait; +use PTS\Events\Bus\EventBusTrait; class Service { use EventBusTrait; diff --git a/benchmark/bechmark.php b/benchmark/bechmark.php index 39d697d..37d863f 100644 --- a/benchmark/bechmark.php +++ b/benchmark/bechmark.php @@ -3,11 +3,13 @@ use Blackfire\Client; use Blackfire\Profile\Configuration; use PTS\Events\EventEmitter; +use PTS\Events\EventEmitterExtraArgs; use PTS\Events\Filter\FilterEmitter; +use PTS\Events\Filter\FilterExtraArgsEmitter; require_once __DIR__ .'/../vendor/autoload.php'; -$iterations = $argv[1] ?? 10000; +$iterations = $argv[1] ?? 800000; $blackfire = $argv[2] ?? false; $iterations++; @@ -17,24 +19,84 @@ } $startTime = microtime(true); + $events = new EventEmitter; +$events2 = new EventEmitterExtraArgs(); + $filters = new FilterEmitter; +$filters2 = new FilterExtraArgsEmitter; $events - ->on('event-a', fn(int $a) => $a, 50, [1, 2]) - ->on('event-a', fn(int $b) => $b); + ->on('event-a', fn(int|null $b = null) => $b) + ->on('event-a', fn(int|null $b = null) => $b); + +$events2 + ->on('event-a', fn(int|null $a = null) => $a, 50, [1, 2]) + ->on('event-a', fn(int|null $b = null) => $b); $filters - ->on('filter-a', fn(int $a, int $b) => $a, 50, [1, 2]) - ->on('filter-a', fn(int $a, int $b) => $a); + ->on('filter-a', fn(int $a = 1, int $b = 2) => $a, 50, [1, 2]) + ->on('filter-a', fn(int $a = 1, int $b = 2) => $a); + +$filters2 + ->on('filter-a', fn(int $a = 1, int $b = 2) => $a, 50, [1, 2]) + ->on('filter-a', fn(int $a = 1, int $b = 2) => $a); + + +function test(string $title, callable $func, int $iterations) { + $startTime = microtime(true); + + while ($iterations--) { + $func(); + } -while ($iterations--) { + $diff = (microtime(true) - $startTime) * 1000; + $duration = sprintf('%2.3f ms', $diff); + echo $title . ': ' . $duration . PHP_EOL; +} + +echo 'Events:' . PHP_EOL; + +test('with args in emit', function() use ($events){ $events->emit('event-a', [1]); +}, $iterations); + +test('EA with args in emit', function() use ($events2){ + $events2->emit('event-a', [1]); +}, $iterations); + + +test('without args in emit', function() use ($events){ + $events->emit('event-a'); +}, $iterations); + +test('EA without args in emit', function() use ($events2){ + $events2->emit('event-a'); +}, $iterations); + + + +echo PHP_EOL . 'Filter:' . PHP_EOL; + +test('with args in emit', function() use ($filters){ $filters->emit('filter-a', 1, [3]); -} +}, $iterations); + +test('EA with args in emit', function() use ($filters2){ + $filters2->emit('filter-a', 1, [3]); +}, $iterations); + + +test('without args in emit', function() use ($filters){ + $filters->emit('filter-a', 1); +}, $iterations); + +test('EA without args in emit', function() use ($filters2){ + $filters2->emit('filter-a', 1); +}, $iterations); + + -$diff = (microtime(true) - $startTime) * 1000; -echo sprintf('%2.3f ms', $diff); echo "\n" . memory_get_peak_usage()/1024; if ($blackfire) { diff --git a/composer.json b/composer.json index 20b9b4c..cb9702c 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,11 @@ "PTS\\Events\\": "src" } }, + "autoload-dev": { + "psr-4": { + "PTS\\Events\\Test\\": "test/unit" + } + }, "scripts": { "bench": "vendor/bin/phpbench run --config=test/phpbench.json --report=aggregate", "test": "vendor/bin/phpunit --config=test/phpunit.xml" diff --git a/src/EventBusTrait.php b/src/Bus/EventBusTrait.php similarity index 92% rename from src/EventBusTrait.php rename to src/Bus/EventBusTrait.php index 420f1ff..4da439f 100644 --- a/src/EventBusTrait.php +++ b/src/Bus/EventBusTrait.php @@ -1,8 +1,9 @@ listeners[$name] ?? [] as $i => $listener) { + match ($countArgs) { + 0 => ($listener->handler)(...$listener->extraArgs), + 1 => ($listener->handler)($args[0], ...$listener->extraArgs), + 2 => ($listener->handler)($args[0], $args[1], ...$listener->extraArgs), + default => ($listener->handler)(...$args, ...$listener->extraArgs), + }; + + if ($listener->once) { + unset($this->listeners[$name][$i]); + if (count($this->listeners[$name]) === 0) { + unset($this->listeners[$name]); + } + } + } + } catch (StopPropagation) { + return; + } + } + + public function emitNoArgs(string $name): void + { + try { + foreach ($this->listeners[$name] ?? [] as $i => $listener) { + ($listener->handler)(...$listener->extraArgs); + + if ($listener->once) { + unset($this->listeners[$name][$i]); + if (count($this->listeners[$name]) === 0) { + unset($this->listeners[$name]); + } + } + } + } catch (StopPropagation) { + return; + } + } +} diff --git a/src/EventEmitterInterface.php b/src/EventEmitterInterface.php index 89b7cdc..acd2a9f 100644 --- a/src/EventEmitterInterface.php +++ b/src/EventEmitterInterface.php @@ -6,6 +6,8 @@ interface EventEmitterInterface { public function emit(string $name, array $args = []): void; + public function emitNoArgs(string $name): void; + public function on(string $name, callable $handler, int $priority = 50, array $extraArgs = []): static; public function once(string $name, callable $handler, int $priority = 50, array $extraArgs = []): static; public function off(string $event, callable $handler = null, int $priority = null): static; diff --git a/src/EventEmitterTrait.php b/src/EventEmitterTrait.php index ab26001..121afcf 100644 --- a/src/EventEmitterTrait.php +++ b/src/EventEmitterTrait.php @@ -9,11 +9,35 @@ trait EventEmitterTrait protected array $listeners = []; public function emit(string $name, array $args = []): void + { + $countArgs = count($args); + + try { + foreach ($this->listeners[$name] ?? [] as $i => $listener) { + match ($countArgs) { + 0 => ($listener->handler)(), + 1 => ($listener->handler)($args[0]), + 2 => ($listener->handler)($args[0], $args[1]), + default => ($listener->handler)(...$args), + }; + + if ($listener->once) { + unset($this->listeners[$name][$i]); + if (count($this->listeners[$name]) === 0) { + unset($this->listeners[$name]); + } + } + } + } catch (StopPropagation) { + return; + } + } + + public function emitNoArgs(string $name): void { try { foreach ($this->listeners[$name] ?? [] as $i => $listener) { - $handler = $listener->handler; - $handler(...$args, ...$listener->extraArgs); + ($listener->handler)(); if ($listener->once) { unset($this->listeners[$name][$i]); diff --git a/src/EventExtraArgsEmitter.php b/src/EventExtraArgsEmitter.php new file mode 100644 index 0000000..fade972 --- /dev/null +++ b/src/EventExtraArgsEmitter.php @@ -0,0 +1,9 @@ +id = $this->getHandlerId($handler); $this->handler = $handler; diff --git a/src/Filter/FilterEmitterExtraArgsTrait.php b/src/Filter/FilterEmitterExtraArgsTrait.php new file mode 100644 index 0000000..8fbc653 --- /dev/null +++ b/src/Filter/FilterEmitterExtraArgsTrait.php @@ -0,0 +1,60 @@ +listeners[$name] ?? [] as $i => $listener) { + $value = match ($countArgs) { + 0 => ($listener->handler)($value, ...$listener->extraArgs), + 1 => ($listener->handler)($value, $args[0], ...$listener->extraArgs), + 2 => ($listener->handler)($value, $args[0], $args[1], ...$listener->extraArgs), + 3 => ($listener->handler)($value, $args[0], $args[1], $args[2], ...$listener->extraArgs), + default => ($listener->handler)($value, ...$args, ...$listener->extraArgs), + }; + + if ($listener->once) { + unset($this->listeners[$name][$i]); + if (count($this->listeners[$name]) === 0) { + unset($this->listeners[$name]); + } + } + } + } catch (StopPropagation $e) { + return $e->getValue(); + } + + return $value; + } + + public function emitNoArgs(string $name, mixed $value): mixed + { + try { + foreach ($this->listeners[$name] ?? [] as $i => $listener) { + $value = ($listener->handler)($value, ...$listener->extraArgs); + + if ($listener->once) { + unset($this->listeners[$name][$i]); + if (count($this->listeners[$name]) === 0) { + unset($this->listeners[$name]); + } + } + } + } catch (StopPropagation $e) { + return $e->getValue(); + } + + return $value; + } +} diff --git a/src/Filter/FilterEmitterInterface.php b/src/Filter/FilterEmitterInterface.php index b1d8144..e31873d 100644 --- a/src/Filter/FilterEmitterInterface.php +++ b/src/Filter/FilterEmitterInterface.php @@ -8,6 +8,8 @@ interface FilterEmitterInterface { public function emit(string $name, mixed $value, array $args = []): mixed; + public function emitNoArgs(string $name, mixed $value): mixed; + public function on(string $name, callable $handler, int $priority = 50, array $extraArgs = []): static; public function once(string $name, callable $handler, int $priority = 50, array $extraArgs = []): static; public function off(string $event, callable $handler = null, int $priority = null): static; diff --git a/src/Filter/FilterEmitterTrait.php b/src/Filter/FilterEmitterTrait.php index a1abf2c..6bee18b 100644 --- a/src/Filter/FilterEmitterTrait.php +++ b/src/Filter/FilterEmitterTrait.php @@ -11,11 +11,38 @@ trait FilterEmitterTrait use EventEmitterTrait; public function emit(string $name, mixed $value, array $args = []): mixed + { + $countArgs = count($args); + + try { + foreach ($this->listeners[$name] ?? [] as $i => $listener) { + $value = match ($countArgs) { + 0 => ($listener->handler)($value), + 1 => ($listener->handler)($value, $args[0]), + 2 => ($listener->handler)($value, $args[0], $args[1]), + 3 => ($listener->handler)($value, $args[0], $args[1], $args[2]), + default => ($listener->handler)($value, ...$args), + }; + + if ($listener->once) { + unset($this->listeners[$name][$i]); + if (count($this->listeners[$name]) === 0) { + unset($this->listeners[$name]); + } + } + } + } catch (StopPropagation $e) { + return $e->getValue(); + } + + return $value; + } + + public function emitNoArgs(string $name, mixed $value): mixed { try { foreach ($this->listeners[$name] ?? [] as $i => $listener) { - $handler = $listener->handler; - $value = $handler($value, ...$args, ...$listener->extraArgs); + $value = ($listener->handler)($value); if ($listener->once) { unset($this->listeners[$name][$i]); diff --git a/src/Filter/FilterExtraArgsEmitter.php b/src/Filter/FilterExtraArgsEmitter.php new file mode 100644 index 0000000..acba98b --- /dev/null +++ b/src/Filter/FilterExtraArgsEmitter.php @@ -0,0 +1,9 @@ +emitter = new EventEmitter; + $this->emitterEA = new EventEmitterExtraArgs; + $this->emitter->on('some.event', static fn() => 1); + $this->emitterEA->on('some.event', static fn() => 1); } - + /** - * @Subject bench - * @Revs(10000000) - * @Iterations(10) + * @Subject event emit + * @Revs(100000) + * @Iterations(20) * @BeforeMethods({"init"}) * @OutputTimeUnit("microseconds", precision=3) * @OutputMode("throughput") @@ -34,4 +40,46 @@ public function emit(): void { $this->emitter->emit('some.event'); } + + /** + * @Subject EA: event emit + * @Revs(100000) + * @Iterations(20) + * @BeforeMethods({"init"}) + * @OutputTimeUnit("microseconds", precision=3) + * @OutputMode("throughput") + * @Warmup(1) + */ + public function EA_emit(): void + { + $this->emitterEA->emit('some.event'); + } + + /** + * @Subject event emit no args + * @Revs(100000) + * @Iterations(20) + * @BeforeMethods({"init"}) + * @OutputTimeUnit("microseconds", precision=3) + * @OutputMode("throughput") + * @Warmup(1) + */ + public function emitNoArgs(): void + { + $this->emitter->emitNoArgs('some.event'); + } + + /** + * @Subject EA: event emitEA no args + * @Revs(100000) + * @Iterations(20) + * @BeforeMethods({"init"}) + * @OutputTimeUnit("microseconds", precision=3) + * @OutputMode("throughput") + * @Warmup(1) + */ + public function EA_emitNoArgs(): void + { + $this->emitterEA->emitNoArgs('some.event'); + } } diff --git a/test/benchmarks/FilterBench.php b/test/benchmarks/FilterBench.php new file mode 100644 index 0000000..355d7fb --- /dev/null +++ b/test/benchmarks/FilterBench.php @@ -0,0 +1,52 @@ +filters = new FilterEmitter; + $this->filters->on('some.filter', static fn($i) => $i++); + } + + /** + * @Subject event emit + * @Revs(100000) + * @Iterations(20) + * @BeforeMethods({"init"}) + * @OutputTimeUnit("microseconds", precision=3) + * @OutputMode("throughput") + * @Warmup(1) + */ + public function emit(): void + { + $this->filters->emit('some.filter', 1); + } + + /** + * @Subject event emit no args + * @Revs(100000) + * @Iterations(20) + * @BeforeMethods({"init"}) + * @OutputTimeUnit("microseconds", precision=3) + * @OutputMode("throughput") + * @Warmup(1) + */ + public function emitNoArgs(): void + { + $this->filters->emitNoArgs('some.event', 1); + } +} diff --git a/test/phpunit.xml b/test/phpunit.xml index 495894c..f865de1 100644 --- a/test/phpunit.xml +++ b/test/phpunit.xml @@ -9,8 +9,14 @@ - - ./unit + + ./unit/Event + + + ./unit/Filter + + + ./unit/Bus diff --git a/test/unit/EmitterTraitTest.php b/test/unit/Bus/EventBusTraitTest.php similarity index 93% rename from test/unit/EmitterTraitTest.php rename to test/unit/Bus/EventBusTraitTest.php index 7a49fcc..782fdf3 100644 --- a/test/unit/EmitterTraitTest.php +++ b/test/unit/Bus/EventBusTraitTest.php @@ -1,10 +1,11 @@ events = new EventEmitterExtraArgs; + $this->buffer = null; + } + + public function testEmitNoArgs(): void + { + $handler = Closure::bind(function(string $title = 'default') { + $this->buffer = $title; + }, $this, get_class($this)); + + $this->events->on('some:event', $handler, 50, ['extraArg']); + + $this->events->emitNoArgs('some:event'); + static::assertEquals('extraArg', $this->buffer); + } +} diff --git a/test/unit/EventsTest.php b/test/unit/Event/EventsEmitterTest.php similarity index 58% rename from test/unit/EventsTest.php rename to test/unit/Event/EventsEmitterTest.php index 8748639..1d5302c 100644 --- a/test/unit/EventsTest.php +++ b/test/unit/Event/EventsEmitterTest.php @@ -1,6 +1,7 @@ buffer); } + public function testDifferentCountArguments(): void + { + $handler = Closure::bind(function(...$args) { + $this->buffer = array_sum($args); + }, $this, get_class($this)); + + $this->events->on('some:event', $handler); + + $this->events->emit('some:event', []); + static::assertEquals(0, $this->buffer); + + $this->events->emit('some:event', [2]); + static::assertEquals(2, $this->buffer); + + $this->events->emit('some:event', [2, 4]); + static::assertEquals(6, $this->buffer); + + $this->events->emit('some:event', [2, 4, 1]); + static::assertEquals(7, $this->buffer); + + $this->events->emit('some:event', [2, 4, 1, 1]); + static::assertEquals(8, $this->buffer); + } + + public function testEmitNoArgs(): void + { + $handler = Closure::bind(function(string $title = 'default') { + $this->buffer = $title; + }, $this, get_class($this)); + + $this->events->on('some:event', $handler, 50, ['extraArg']); + + $this->events->emitNoArgs('some:event'); + static::assertEquals('default', $this->buffer); + } + public function testEventWithoutListeners(): void { $this->events->emit('some:event'); @@ -70,10 +106,15 @@ public function testChain(): void { $expected = EventEmitterInterface::class; static::assertInstanceOf($expected, $this->events->on('some', [$this, 'customEventHandler'])); + static::assertInstanceOf($expected, $this->events->once('some', [$this, 'customEventHandler'])); static::assertInstanceOf($expected, $this->events->off('some', [$this, 'customEventHandler'])); } - public function testStopPropagation(): void + /** + * @return void + * @dataProvider stopPropagationDataProvider + */ + public function testStopPropagation(string $method): void { $handler = Closure::bind(function () { $this->buffer = 'closure'; @@ -86,11 +127,20 @@ public function testStopPropagation(): void $this->events->on('name', $handler); $this->events->on('name', $handler2); - $this->events->emit('name'); + + $this->events->{$method}('name'); static::assertEquals('closure', $this->buffer); } + public function stopPropagationDataProvider(): array + { + return [ + ['emit'] , + ['emitNoArgs'] + ]; + } + public function testEventNames(): void { static::assertCount(0, $this->events->eventNames()); @@ -105,13 +155,16 @@ public function testOnce(): void $obj = new stdClass; $obj->count = 0; - $this->events->once('once:test', function (stdClass $obj) { - $obj->count++; - }); + $this->events->once('once:test', fn(stdClass $obj) => $obj->count++); static::assertCount(1, $this->events->listeners()); $this->events->emit('once:test', [$obj]); static::assertCount(0, $this->events->listeners()); static::assertSame(1, $obj->count); + + $this->events->once('once:test2', function() { }); + static::assertCount(1, $this->events->listeners()); + $this->events->emitNoArgs('once:test2'); + static::assertCount(0, $this->events->listeners()); } } diff --git a/test/unit/Filter/FiltersEmitterTest.php b/test/unit/Filter/FiltersEmitterTest.php new file mode 100644 index 0000000..d876edc --- /dev/null +++ b/test/unit/Filter/FiltersEmitterTest.php @@ -0,0 +1,42 @@ +filters = new FilterEmitter; + } + + public function testFilterWithExtraOnArguments(): void + { + $rawTitle = 'Hello world!!! '; + $this->filters->on('before_output_title', [$this, 'customFilterHandler'], 50, [10]); + $title = $this->filters->emit('before_output_title', $rawTitle); + + self::assertEquals('Hell', $title, 'must skip extra args from handler'); + } + + public function testEmitNoArgs(): void + { + $handler = Closure::bind(function(string $value, ?string $extra = null) { + return $extra ?? $value; + }, $this, get_class($this)); + + $this->filters->on('some:event', $handler, 50, ['extraArg']); + + $actual = $this->filters->emitNoArgs('some:event', 'alex'); + static::assertEquals('alex', $actual); + } +} diff --git a/test/unit/FiltersTest.php b/test/unit/Filter/FiltersExtraArgsEmitterTest.php similarity index 67% rename from test/unit/FiltersTest.php rename to test/unit/Filter/FiltersExtraArgsEmitterTest.php index 8437259..1fd2148 100644 --- a/test/unit/FiltersTest.php +++ b/test/unit/Filter/FiltersExtraArgsEmitterTest.php @@ -1,13 +1,15 @@ filters = new FilterEmitter; + $this->filters = new FilterExtraArgsEmitter; } public function customFilterHandler(string $value, int $length = 4): string @@ -118,7 +120,11 @@ public function testChain(): void self::assertInstanceOf(FilterEmitterInterface::class, $this->filters->off('some', 'trim')); } - public function testStopPropagation(): void + /** + * @return void + * @dataProvider stopPropagationDataProvider + */ + public function testStopPropagation(string $method): void { $rawTitle = ' Hello world!!! '; $this->filters->on('before_output_title', 'trim'); @@ -126,18 +132,71 @@ public function testStopPropagation(): void throw (new StopPropagation)->setValue($value); }); $this->filters->on('before_output_title', [$this, 'customFilterHandler']); - $title = $this->filters->emit('before_output_title', $rawTitle); + $title = $this->filters->{$method}('before_output_title', $rawTitle); self::assertEquals('Hello world!!!', $title); } + public function stopPropagationDataProvider(): array + { + return [ + ['emit'], + ['emitNoArgs'], + ]; + } + public function testOnceHandler(): void { $rawTitle = ' Hello world!!! '; $this->filters->once('before_output_title', 'trim'); - $this->filters->emit('before_output_title', $rawTitle); $title = $this->filters->emit('before_output_title', $rawTitle); + self::assertEquals('Hello world!!!', $title); + $title = $this->filters->emit('before_output_title', $rawTitle); self::assertEquals($rawTitle, $title); + + // noArgs + $this->filters->once('before_output_title', 'trim'); + $title = $this->filters->emitNoArgs('before_output_title', $rawTitle); + self::assertEquals('Hello world!!!', $title); + + $title = $this->filters->emitNoArgs('before_output_title', $rawTitle); + self::assertEquals($rawTitle, $title); + } + + public function testDifferentCountArguments(): void + { + $handler = Closure::bind(function(int $value, ...$args) { + return $value + array_sum($args); + }, $this, get_class($this)); + + $this->filters->on('some:event', $handler); + + $actual = $this->filters->emit('some:event', 2, []); + static::assertEquals(2, $actual); + + $actual = $this->filters->emit('some:event', 2, [2]); + static::assertEquals(4, $actual); + + $actual = $this->filters->emit('some:event', 2, [2, 4]); + static::assertEquals(8, $actual); + + $actual = $this->filters->emit('some:event', 2, [2, 4, 1]); + static::assertEquals(9, $actual); + + $actual = $this->filters->emit('some:event', 2, [2, 4, 1, 1]); + static::assertEquals(10, $actual); + } + + public function testEmitNoArgs(): void + { + $handler = Closure::bind(function(string $value, ?string $extra) { + return $extra ?? $value; + }, $this, get_class($this)); + + $this->filters->on('some:event', $handler, 50, ['extraArg']); + + $actual = $this->filters->emitNoArgs('some:event', 'alex'); + static::assertEquals('extraArg', $actual); } }