From 53948d30a46112b08e9a18faa464e4131f8a8d62 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 6 Nov 2024 22:02:09 +1300 Subject: [PATCH 01/15] Initial commit --- .github/workflows/ci.yml | 54 ++++++++++++ .gitignore | 11 +++ .php-cs-fixer.dist.php | 25 ++++++ README.md | 17 ++++ composer.json | 44 ++++++++++ examples/basic-usage.php | 42 +++++++++ phpstan.neon | 9 ++ phpunit.xml.dist | 24 ++++++ src/AsyncEventDispatcher.php | 102 ++++++++++++++++++++++ src/Event/AbstractEvent.php | 36 ++++++++ src/ListenerProvider.php | 41 +++++++++ tests/AsyncEventDispatcherTest.php | 134 +++++++++++++++++++++++++++++ tests/Fixture/TestEvent.php | 23 +++++ 13 files changed, 562 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .php-cs-fixer.dist.php create mode 100644 README.md create mode 100644 composer.json create mode 100644 examples/basic-usage.php create mode 100644 phpstan.neon create mode 100644 phpunit.xml.dist create mode 100644 src/AsyncEventDispatcher.php create mode 100644 src/Event/AbstractEvent.php create mode 100644 src/ListenerProvider.php create mode 100644 tests/AsyncEventDispatcherTest.php create mode 100644 tests/Fixture/TestEvent.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..251dce7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + tests: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + matrix: + php: ['8.1', '8.2', '8.3'] + + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run tests + run: vendor/bin/phpunit + + - name: Static Analysis + run: vendor/bin/phpstan analyse + + coding-standards: + name: Coding Standards + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + coverage: none + tools: composer:v2, php-cs-fixer + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Check coding standards + run: php-cs-fixer fix --dry-run --diff \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7390ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/vendor/ +/composer.lock +/.phpunit.cache/ +/.phpunit.result.cache +/.php-cs-cache +/phpstan.cache +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..37e3b43 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,25 @@ +in([ + __DIR__ . '/src', + __DIR__ . '/tests', + __DIR__ . '/examples', + ]) + ->name('*.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'declare_strict_types' => true, + 'strict_param' => true, + 'no_extra_blank_lines' => true, + 'single_quote' => true, + 'trailing_comma_in_multiline' => true, + ]) + ->setFinder($finder); \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a42b3c4 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# PSR-14 Async Event Dispatcher (Experimental) + +A PSR-14 compatible event dispatcher implementation using Revolt and AMPHP for asynchronous event handling. + +## Features + +- Full PSR-14 compatibility +- Asynchronous event dispatching using Revolt's event loop +- Support for stoppable and non-stoppable events +- Fire-and-forget event dispatching +- Type-safe event handling + +## Installation + +```bash +composer require archipro/revolt-event-dispatcher +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..13786cc --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "archipro/revolt-event-dispatcher", + "description": "PSR-14 Event Dispatcher implementation using Revolt and AMPHP", + "authors": [ + { + "name": "ArchiPro", + "email": "developers@archipro.co.nz" + } + ], + "type": "library", + "require": { + "php": "^8.1", + "psr/event-dispatcher": "^1.0", + "revolt/event-loop": "^1.0", + "amphp/amp": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "phpstan/phpstan": "^1.10", + "friendsofphp/php-cs-fixer": "^3.14" + }, + "autoload": { + "psr-4": { + "ArchiPro\\EventDispatcher\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "ArchiPro\\EventDispatcher\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "phpunit", + "analyse": "phpstan analyse", + "cs-check": "php-cs-fixer fix --dry-run --diff", + "cs-fix": "php-cs-fixer fix", + "check": [ + "@cs-check", + "@analyse", + "@test" + ] + }, + "license": "MIT" +} diff --git a/examples/basic-usage.php b/examples/basic-usage.php new file mode 100644 index 0000000..9c1b48b --- /dev/null +++ b/examples/basic-usage.php @@ -0,0 +1,42 @@ +addListener(UserCreatedEvent::class, function (UserCreatedEvent $event) { + // Simulate async operation + EventLoop::delay(1, fn() => null); + echo "Sending welcome email to {$event->email}\n"; +}); + +$listenerProvider->addListener(UserCreatedEvent::class, function (UserCreatedEvent $event) { + // Simulate async operation + EventLoop::delay(0.5, fn() => null); + echo "Logging user creation: {$event->userId}\n"; +}); + +// Create the event dispatcher +$dispatcher = new AsyncEventDispatcher($listenerProvider); + +// Dispatch an event +$event = new UserCreatedEvent('123', 'user@example.com'); +$dispatcher->dispatch($event); + +// Run the event loop +EventLoop::run(); \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..00ce8b5 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +parameters: + level: 8 + paths: + - src + - tests + - examples + tmpDir: phpstan.cache + checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..00ffdd4 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + tests + + + + + + src + + + \ No newline at end of file diff --git a/src/AsyncEventDispatcher.php b/src/AsyncEventDispatcher.php new file mode 100644 index 0000000..f207b6c --- /dev/null +++ b/src/AsyncEventDispatcher.php @@ -0,0 +1,102 @@ +listenerProvider = $listenerProvider; + } + + /** + * Dispatches an event to all registered listeners asynchronously. + * + * Each listener is scheduled in the event loop and executed asynchronously. + * The method returns immediately without waiting for listeners to complete. + * If the event implements StoppableEventInterface, propagation can be stopped + * to prevent subsequent listeners from being scheduled. + * + * @param object $event The event to dispatch + * @return object The dispatched event + */ + public function dispatch(object $event): object + { + $listeners = $this->listenerProvider->getListenersForEvent($event); + + if ($event instanceof StoppableEventInterface) { + return $this->dispatchStoppableEvent($event, $listeners); + } + + return $this->dispatchNonStoppableEvent($event, $listeners); + } + + /** + * Dispatches a stoppable event to listeners asynchronously. + * Uses a queue to handle propagation stopping. + * + * @param StoppableEventInterface $event + * @param iterable $listeners + * @return StoppableEventInterface + */ + private function dispatchStoppableEvent(StoppableEventInterface $event, iterable $listeners): StoppableEventInterface + { + async(function() use ($event, $listeners): void { + foreach ($listeners as $listener) { + $listener($event); + if ($event->isPropagationStopped()) { + break; + } + } + }); + + return $event; + } + + /** + * Dispatches a non-stoppable event to listeners asynchronously. + * Simply queues all listeners in the event loop. + * + * Because we don't need to worry about stopping propagation, we can simply + * queue all listeners in the event loop and let them run whenever in any order. + * + * @param object $event + * @param iterable $listeners + * @return object + */ + private function dispatchNonStoppableEvent(object $event, iterable $listeners): object + { + foreach ($listeners as $listener) { + EventLoop::queue(function() use ($event, $listener) { + $listener($event); + }); + } + + return $event; + } + + + +} \ No newline at end of file diff --git a/src/Event/AbstractEvent.php b/src/Event/AbstractEvent.php new file mode 100644 index 0000000..b08ba30 --- /dev/null +++ b/src/Event/AbstractEvent.php @@ -0,0 +1,36 @@ +propagationStopped; + } + + /** + * Stops the propagation of the event to subsequent listeners. + */ + public function stopPropagation(): void + { + $this->propagationStopped = true; + } +} \ No newline at end of file diff --git a/src/ListenerProvider.php b/src/ListenerProvider.php new file mode 100644 index 0000000..f5d81be --- /dev/null +++ b/src/ListenerProvider.php @@ -0,0 +1,41 @@ +> */ + private array $listeners = []; + + /** + * Registers a listener for a specific event class. + * + * @param string $eventClass The fully qualified class name of the event + * @param callable $listener The listener callback that will handle the event + */ + public function addListener(string $eventClass, callable $listener): void + { + $this->listeners[$eventClass][] = $listener; + } + + /** + * Gets all listeners registered for the given event. + * + * @param object $event The event to get listeners for + * @return iterable The registered listeners + */ + public function getListenersForEvent(object $event): iterable + { + return $this->listeners[$event::class] ?? []; + } +} \ No newline at end of file diff --git a/tests/AsyncEventDispatcherTest.php b/tests/AsyncEventDispatcherTest.php new file mode 100644 index 0000000..997b37f --- /dev/null +++ b/tests/AsyncEventDispatcherTest.php @@ -0,0 +1,134 @@ +listenerProvider = new ListenerProvider(); + $this->dispatcher = new AsyncEventDispatcher($this->listenerProvider); + + EventLoop::setErrorHandler(function (\Throwable $err) { + throw $err; + }); + + } + + /** + * Tests that multiple listeners for an event are executed. + */ + public function testDispatchEventToMultipleListeners(): void + { + $results = []; + $completed = false; + + $this->listenerProvider->addListener(TestEvent::class, function (TestEvent $event) use (&$results) { + EventLoop::delay(0.1, fn() => null); + $results[] = 'listener1: ' . $event->data; + }); + + $this->listenerProvider->addListener(TestEvent::class, function (TestEvent $event) use (&$results, &$completed) { + EventLoop::delay(0.05, fn() => null); + $results[] = 'listener2: ' . $event->data; + $completed = true; + }); + + $event = new TestEvent('test data'); + $this->dispatcher->dispatch($event); + + // Verify immediate return + $this->assertEmpty($results); + + // Run the event loop until listeners complete + EventLoop::run(); + + $this->assertTrue($completed); + $this->assertCount(2, $results); + $this->assertContains('listener1: test data', $results); + $this->assertContains('listener2: test data', $results); + } + + /** + * Tests that event propagation can be stopped synchronously. + */ + public function testSynchronousStoppableEvent(): void + { + $results = []; + + $this->listenerProvider->addListener(TestEvent::class, function (TestEvent $event) use (&$results) { + $results[] = 'listener1'; + $event->stopPropagation(); + }); + + $this->listenerProvider->addListener(TestEvent::class, function (TestEvent $event) use (&$results) { + $results[] = 'listener2'; + }); + + $event = new TestEvent('test data'); + $this->dispatcher->dispatch($event); + EventLoop::run(); + + $this->assertCount(1, $results); + $this->assertEquals(['listener1'], $results); + } + + + /** + * Tests handling of events with no registered listeners. + */ + public function testNoListenersForEvent(): void + { + $event = new TestEvent('test data'); + $dispatchedEvent = $this->dispatcher->dispatch($event); + + $this->assertSame($event, $dispatchedEvent); + } + + /** + * @test + */ + public function dispatchesNonStoppableEvents(): void + { + $event = new class() { + public $called = false; + }; + + $listener = function ($event) { + + $event->called = true; + }; + + $this->listenerProvider->addListener(get_class($event), $listener); + $this->dispatcher->dispatch($event); + + EventLoop::run(); + + $this->assertTrue($event->called, 'Listener should have been called for non-stoppable event'); + } + +} \ No newline at end of file diff --git a/tests/Fixture/TestEvent.php b/tests/Fixture/TestEvent.php new file mode 100644 index 0000000..f19d9f8 --- /dev/null +++ b/tests/Fixture/TestEvent.php @@ -0,0 +1,23 @@ + Date: Wed, 6 Nov 2024 22:09:02 +1300 Subject: [PATCH 02/15] Allow workflow to be run manually --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 251dce7..5682eb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [ main ] pull_request: branches: [ main ] + workflow_dispatch: jobs: tests: From 5465fa71f010a0412d50390ac13e979cf15ae353 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 6 Nov 2024 22:12:44 +1300 Subject: [PATCH 03/15] Fix build issue --- .php-cs-fixer.dist.php | 2 -- phpstan.neon | 3 +-- tests/AsyncEventDispatcherTest.php | 7 ++++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 37e3b43..918fbb2 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -16,8 +16,6 @@ 'array_syntax' => ['syntax' => 'short'], 'ordered_imports' => ['sort_algorithm' => 'alpha'], 'no_unused_imports' => true, - 'declare_strict_types' => true, - 'strict_param' => true, 'no_extra_blank_lines' => true, 'single_quote' => true, 'trailing_comma_in_multiline' => true, diff --git a/phpstan.neon b/phpstan.neon index 00ce8b5..306d3c0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,5 +5,4 @@ parameters: - tests - examples tmpDir: phpstan.cache - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false \ No newline at end of file + checkMissingIterableValueType: false \ No newline at end of file diff --git a/tests/AsyncEventDispatcherTest.php b/tests/AsyncEventDispatcherTest.php index 997b37f..eb1cff8 100644 --- a/tests/AsyncEventDispatcherTest.php +++ b/tests/AsyncEventDispatcherTest.php @@ -112,20 +112,21 @@ public function testNoListenersForEvent(): void /** * @test */ - public function dispatchesNonStoppableEvents(): void + public function testDispatchesNonStoppableEvents(): void { $event = new class() { - public $called = false; + public bool $called = false; }; $listener = function ($event) { - $event->called = true; }; $this->listenerProvider->addListener(get_class($event), $listener); $this->dispatcher->dispatch($event); + $this->assertFalse($event->called, 'Listener should not have been called right away'); + EventLoop::run(); $this->assertTrue($event->called, 'Listener should have been called for non-stoppable event'); From 3d23fa82d518f4f7b5c031a5f8f549a41d54b384 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 6 Nov 2024 22:13:34 +1300 Subject: [PATCH 04/15] Target master instead of main --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5682eb9..eb97501 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [ master ] pull_request: - branches: [ main ] + branches: [ master ] workflow_dispatch: jobs: From a9d132273bb5ffc20b6dee6d1e83ab361e41b472 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 6 Nov 2024 22:15:46 +1300 Subject: [PATCH 05/15] Fix linting warning --- .gitignore | 3 ++- examples/basic-usage.php | 11 ++++++----- src/AsyncEventDispatcher.php | 19 +++++++++---------- src/Event/AbstractEvent.php | 2 +- src/ListenerProvider.php | 2 +- tests/AsyncEventDispatcherTest.php | 19 +++++++++---------- tests/Fixture/TestEvent.php | 5 +++-- 7 files changed, 31 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index f7390ad..eaea388 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ .vscode/ *.swp *.swo -.DS_Store \ No newline at end of file +.DS_Store +.php-cs-fixer.cache \ No newline at end of file diff --git a/examples/basic-usage.php b/examples/basic-usage.php index 9c1b48b..b91c674 100644 --- a/examples/basic-usage.php +++ b/examples/basic-usage.php @@ -3,8 +3,8 @@ require __DIR__ . '/../vendor/autoload.php'; use ArchiPro\EventDispatcher\AsyncEventDispatcher; -use ArchiPro\EventDispatcher\ListenerProvider; use ArchiPro\EventDispatcher\Event\AbstractEvent; +use ArchiPro\EventDispatcher\ListenerProvider; use Revolt\EventLoop; // Create a custom event @@ -13,7 +13,8 @@ class UserCreatedEvent extends AbstractEvent public function __construct( public readonly string $userId, public readonly string $email - ) {} + ) { + } } // Create the listener provider and register listeners @@ -21,13 +22,13 @@ public function __construct( $listenerProvider->addListener(UserCreatedEvent::class, function (UserCreatedEvent $event) { // Simulate async operation - EventLoop::delay(1, fn() => null); + EventLoop::delay(1, fn () => null); echo "Sending welcome email to {$event->email}\n"; }); $listenerProvider->addListener(UserCreatedEvent::class, function (UserCreatedEvent $event) { // Simulate async operation - EventLoop::delay(0.5, fn() => null); + EventLoop::delay(0.5, fn () => null); echo "Logging user creation: {$event->userId}\n"; }); @@ -39,4 +40,4 @@ public function __construct( $dispatcher->dispatch($event); // Run the event loop -EventLoop::run(); \ No newline at end of file +EventLoop::run(); diff --git a/src/AsyncEventDispatcher.php b/src/AsyncEventDispatcher.php index f207b6c..243cbd3 100644 --- a/src/AsyncEventDispatcher.php +++ b/src/AsyncEventDispatcher.php @@ -4,13 +4,14 @@ namespace ArchiPro\EventDispatcher; +use function Amp\async; + +use Amp\Pipeline\Queue; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\EventDispatcher\StoppableEventInterface; -use Revolt\EventLoop; -use Amp\Pipeline\Queue; -use function Amp\async; +use Revolt\EventLoop; /** * Asynchronous implementation of PSR-14 EventDispatcherInterface using Revolt and AMPHP. @@ -49,7 +50,7 @@ public function dispatch(object $event): object if ($event instanceof StoppableEventInterface) { return $this->dispatchStoppableEvent($event, $listeners); } - + return $this->dispatchNonStoppableEvent($event, $listeners); } @@ -63,7 +64,7 @@ public function dispatch(object $event): object */ private function dispatchStoppableEvent(StoppableEventInterface $event, iterable $listeners): StoppableEventInterface { - async(function() use ($event, $listeners): void { + async(function () use ($event, $listeners): void { foreach ($listeners as $listener) { $listener($event); if ($event->isPropagationStopped()) { @@ -78,7 +79,7 @@ private function dispatchStoppableEvent(StoppableEventInterface $event, iterable /** * Dispatches a non-stoppable event to listeners asynchronously. * Simply queues all listeners in the event loop. - * + * * Because we don't need to worry about stopping propagation, we can simply * queue all listeners in the event loop and let them run whenever in any order. * @@ -89,7 +90,7 @@ private function dispatchStoppableEvent(StoppableEventInterface $event, iterable private function dispatchNonStoppableEvent(object $event, iterable $listeners): object { foreach ($listeners as $listener) { - EventLoop::queue(function() use ($event, $listener) { + EventLoop::queue(function () use ($event, $listener) { $listener($event); }); } @@ -97,6 +98,4 @@ private function dispatchNonStoppableEvent(object $event, iterable $listeners): return $event; } - - -} \ No newline at end of file +} diff --git a/src/Event/AbstractEvent.php b/src/Event/AbstractEvent.php index b08ba30..349e63d 100644 --- a/src/Event/AbstractEvent.php +++ b/src/Event/AbstractEvent.php @@ -33,4 +33,4 @@ public function stopPropagation(): void { $this->propagationStopped = true; } -} \ No newline at end of file +} diff --git a/src/ListenerProvider.php b/src/ListenerProvider.php index f5d81be..c79ec87 100644 --- a/src/ListenerProvider.php +++ b/src/ListenerProvider.php @@ -38,4 +38,4 @@ public function getListenersForEvent(object $event): iterable { return $this->listeners[$event::class] ?? []; } -} \ No newline at end of file +} diff --git a/tests/AsyncEventDispatcherTest.php b/tests/AsyncEventDispatcherTest.php index eb1cff8..96a4e3f 100644 --- a/tests/AsyncEventDispatcherTest.php +++ b/tests/AsyncEventDispatcherTest.php @@ -4,10 +4,10 @@ namespace ArchiPro\EventDispatcher\Tests; -use PHPUnit\Framework\TestCase; use ArchiPro\EventDispatcher\AsyncEventDispatcher; use ArchiPro\EventDispatcher\ListenerProvider; use ArchiPro\EventDispatcher\Tests\Fixture\TestEvent; +use PHPUnit\Framework\TestCase; use Revolt\EventLoop; /** @@ -17,7 +17,7 @@ * - Multiple listener execution * - Event propagation stopping * - Handling events with no listeners - * + * * @covers \ArchiPro\EventDispatcher\AsyncEventDispatcher */ class AsyncEventDispatcherTest extends TestCase @@ -46,21 +46,21 @@ public function testDispatchEventToMultipleListeners(): void { $results = []; $completed = false; - + $this->listenerProvider->addListener(TestEvent::class, function (TestEvent $event) use (&$results) { - EventLoop::delay(0.1, fn() => null); + EventLoop::delay(0.1, fn () => null); $results[] = 'listener1: ' . $event->data; }); $this->listenerProvider->addListener(TestEvent::class, function (TestEvent $event) use (&$results, &$completed) { - EventLoop::delay(0.05, fn() => null); + EventLoop::delay(0.05, fn () => null); $results[] = 'listener2: ' . $event->data; $completed = true; }); $event = new TestEvent('test data'); $this->dispatcher->dispatch($event); - + // Verify immediate return $this->assertEmpty($results); @@ -79,7 +79,7 @@ public function testDispatchEventToMultipleListeners(): void public function testSynchronousStoppableEvent(): void { $results = []; - + $this->listenerProvider->addListener(TestEvent::class, function (TestEvent $event) use (&$results) { $results[] = 'listener1'; $event->stopPropagation(); @@ -97,7 +97,6 @@ public function testSynchronousStoppableEvent(): void $this->assertEquals(['listener1'], $results); } - /** * Tests handling of events with no registered listeners. */ @@ -114,7 +113,7 @@ public function testNoListenersForEvent(): void */ public function testDispatchesNonStoppableEvents(): void { - $event = new class() { + $event = new class () { public bool $called = false; }; @@ -132,4 +131,4 @@ public function testDispatchesNonStoppableEvents(): void $this->assertTrue($event->called, 'Listener should have been called for non-stoppable event'); } -} \ No newline at end of file +} diff --git a/tests/Fixture/TestEvent.php b/tests/Fixture/TestEvent.php index f19d9f8..9b5110a 100644 --- a/tests/Fixture/TestEvent.php +++ b/tests/Fixture/TestEvent.php @@ -19,5 +19,6 @@ class TestEvent extends AbstractEvent */ public function __construct( public readonly string $data - ) {} -} \ No newline at end of file + ) { + } +} From 9d96651cbc5981ca20616809f7bba9efae0c3ff4 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 6 Nov 2024 22:17:30 +1300 Subject: [PATCH 06/15] Add CI badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a42b3c4..f7d4353 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # PSR-14 Async Event Dispatcher (Experimental) +[![CI](https://github.com/archipro/revolt-event-dispatcher/actions/workflows/ci.yml/badge.svg)](https://github.com/archipro/revolt-event-dispatcher/actions/workflows/ci.yml) + A PSR-14 compatible event dispatcher implementation using Revolt and AMPHP for asynchronous event handling. ## Features From 9b49c9c65b4da73bbe622a0c15e62063553cd38e Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 6 Nov 2024 22:18:47 +1300 Subject: [PATCH 07/15] Fix the CI badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7d4353..bace6a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PSR-14 Async Event Dispatcher (Experimental) -[![CI](https://github.com/archipro/revolt-event-dispatcher/actions/workflows/ci.yml/badge.svg)](https://github.com/archipro/revolt-event-dispatcher/actions/workflows/ci.yml) +[![CI](https://github.com/archiprocode/revolt-event-dispatcher/actions/workflows/ci.yml/badge.svg)](https://github.com/archiprocode/revolt-event-dispatcher/actions/workflows/ci.yml) A PSR-14 compatible event dispatcher implementation using Revolt and AMPHP for asynchronous event handling. From 3827298e6c731b8ea6eaff544a26122e8a5ce200 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Thu, 7 Nov 2024 11:26:32 +1300 Subject: [PATCH 08/15] Make explicit that we provide a psr/event-dispatcher-implementation --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 13786cc..fdaae84 100644 --- a/composer.json +++ b/composer.json @@ -40,5 +40,8 @@ "@test" ] }, - "license": "MIT" + "license": "MIT", + "provide": { + "psr/event-dispatcher-implementation": "1.0" + } } From 375101de96860bf881157e86e5c999d3a9a1ec1f Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 8 Nov 2024 12:32:19 +1300 Subject: [PATCH 09/15] Remove unused import --- src/AsyncEventDispatcher.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AsyncEventDispatcher.php b/src/AsyncEventDispatcher.php index 243cbd3..8e96199 100644 --- a/src/AsyncEventDispatcher.php +++ b/src/AsyncEventDispatcher.php @@ -6,7 +6,6 @@ use function Amp\async; -use Amp\Pipeline\Queue; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\EventDispatcher\StoppableEventInterface; From b454b26cfa931e5d33323ca99e1e8d07ec53310a Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Mon, 11 Nov 2024 10:43:48 +1300 Subject: [PATCH 10/15] Remove .gitkeep file --- .gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index e69de29..0000000 From 65e0f25185411b1c2e234d4bfa4fff88ec7d11f9 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Mon, 11 Nov 2024 10:47:24 +1300 Subject: [PATCH 11/15] Run builds on everything --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb97501..50a44e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,7 @@ name: CI on: push: - branches: [ master ] pull_request: - branches: [ master ] workflow_dispatch: jobs: @@ -52,4 +50,4 @@ jobs: run: composer install --prefer-dist --no-progress - name: Check coding standards - run: php-cs-fixer fix --dry-run --diff \ No newline at end of file + run: php-cs-fixer fix --dry-run --diff From cc84b4e9779b3899b9f2c5b547f01c5dc3157ce1 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 13 Nov 2024 12:50:21 +1300 Subject: [PATCH 12/15] Apply peer review feedback + update dispatcher to return Futures + make disptach cancellable --- examples/basic-usage.php | 16 ++- src/AsyncEventDispatcher.php | 95 ++++++++++++------ ...ctEvent.php => AbstractStoppableEvent.php} | 2 +- src/ListenerProvider.php | 15 ++- tests/AsyncEventDispatcherTest.php | 97 +++++++++++++++++-- tests/Fixture/TestEvent.php | 4 +- 6 files changed, 176 insertions(+), 53 deletions(-) rename src/Event/{AbstractEvent.php => AbstractStoppableEvent.php} (91%) diff --git a/examples/basic-usage.php b/examples/basic-usage.php index b91c674..0fc51e9 100644 --- a/examples/basic-usage.php +++ b/examples/basic-usage.php @@ -22,14 +22,22 @@ public function __construct( $listenerProvider->addListener(UserCreatedEvent::class, function (UserCreatedEvent $event) { // Simulate async operation - EventLoop::delay(1, fn () => null); - echo "Sending welcome email to {$event->email}\n"; + EventLoop::delay( + 1, + function () use ($event) { + echo "Sending welcome email to {$event->email}\n"; + } + ); }); $listenerProvider->addListener(UserCreatedEvent::class, function (UserCreatedEvent $event) { // Simulate async operation - EventLoop::delay(0.5, fn () => null); - echo "Logging user creation: {$event->userId}\n"; + EventLoop::delay( + 0.5, + function () use ($event) { + echo "Logging user creation: {$event->userId}\n"; + } + ); }); // Create the event dispatcher diff --git a/src/AsyncEventDispatcher.php b/src/AsyncEventDispatcher.php index 8e96199..a4d1d54 100644 --- a/src/AsyncEventDispatcher.php +++ b/src/AsyncEventDispatcher.php @@ -6,73 +6,96 @@ use function Amp\async; +use Amp\Cancellation; +use Amp\Future; + +use function Amp\Future\awaitAll; + +use Amp\NullCancellation; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\EventDispatcher\StoppableEventInterface; -use Revolt\EventLoop; - /** * Asynchronous implementation of PSR-14 EventDispatcherInterface using Revolt and AMPHP. * * This dispatcher schedules event listeners to be executed asynchronously using the Revolt event loop. - * The dispatch method returns immediately without waiting for listeners to complete. + * The dispatch method returns a Future that resolves when all listeners complete. */ class AsyncEventDispatcher implements EventDispatcherInterface { - private ListenerProviderInterface $listenerProvider; - /** * @param ListenerProviderInterface $listenerProvider The provider of event listeners */ public function __construct( - ListenerProviderInterface $listenerProvider + private readonly ListenerProviderInterface $listenerProvider ) { - $this->listenerProvider = $listenerProvider; } /** * Dispatches an event to all registered listeners asynchronously. * * Each listener is scheduled in the event loop and executed asynchronously. - * The method returns immediately without waiting for listeners to complete. + * Returns a Future that resolves with the event once all listeners complete. * If the event implements StoppableEventInterface, propagation can be stopped * to prevent subsequent listeners from being scheduled. * - * @param object $event The event to dispatch - * @return object The dispatched event + * @template T of object + * @param T $event The event to dispatch + * @return Future Future that resolves with the dispatched event */ - public function dispatch(object $event): object + public function dispatch(object $event, ?Cancellation $cancellation = null): Future { $listeners = $this->listenerProvider->getListenersForEvent($event); if ($event instanceof StoppableEventInterface) { - return $this->dispatchStoppableEvent($event, $listeners); + return $this->dispatchStoppableEvent( + $event, + $listeners, + $cancellation ?: new NullCancellation() + ); } - return $this->dispatchNonStoppableEvent($event, $listeners); + return $this->dispatchNonStoppableEvent( + $event, + $listeners, + $cancellation ?: new NullCancellation() + ); } /** * Dispatches a stoppable event to listeners asynchronously. * Uses a queue to handle propagation stopping. * - * @param StoppableEventInterface $event + * @template T of StoppableEventInterface + * @param T $event * @param iterable $listeners - * @return StoppableEventInterface + * @return Future */ - private function dispatchStoppableEvent(StoppableEventInterface $event, iterable $listeners): StoppableEventInterface - { - async(function () use ($event, $listeners): void { + private function dispatchStoppableEvent( + StoppableEventInterface $event, + iterable $listeners, + Cancellation $cancellation + ): Future { + return async(function () use ($event, $listeners, $cancellation): StoppableEventInterface { + // We'll process each listener in sequence so that if one decides to stop propagation, + // we have chance to kill the following listeners. foreach ($listeners as $listener) { - $listener($event); + // We'll wrap our listener in a `async` call. Even if we want to block the next listener in the loop, + // that doesn't mean we want to block other listeners outside this loop. + $future = async(function () use ($event, $listener) { + $listener($event); + }); + + $future->await($cancellation); + + // If one of our listeners decides to stop propagation, we'll break out of the loop. if ($event->isPropagationStopped()) { break; } } + return $event; }); - - return $event; } /** @@ -82,19 +105,29 @@ private function dispatchStoppableEvent(StoppableEventInterface $event, iterable * Because we don't need to worry about stopping propagation, we can simply * queue all listeners in the event loop and let them run whenever in any order. * - * @param object $event + * @template T of object + * @param T $event * @param iterable $listeners - * @return object + * @return Future */ - private function dispatchNonStoppableEvent(object $event, iterable $listeners): object - { - foreach ($listeners as $listener) { - EventLoop::queue(function () use ($event, $listener) { - $listener($event); - }); - } + private function dispatchNonStoppableEvent( + object $event, + iterable $listeners, + Cancellation $cancellation + ): Future { + return async(function () use ($event, $listeners, $cancellation): object { + $futures = []; + foreach ($listeners as $listener) { + $futures[] = async(function () use ($event, $listener) { + $listener($event); + }); + } + + // Wait for all listeners to complete + awaitAll($futures, $cancellation); - return $event; + return $event; + }); } } diff --git a/src/Event/AbstractEvent.php b/src/Event/AbstractStoppableEvent.php similarity index 91% rename from src/Event/AbstractEvent.php rename to src/Event/AbstractStoppableEvent.php index 349e63d..b0bfbc7 100644 --- a/src/Event/AbstractEvent.php +++ b/src/Event/AbstractStoppableEvent.php @@ -12,7 +12,7 @@ * Provides basic functionality for stopping event propagation. Events that need * propagation control should extend this class. */ -abstract class AbstractEvent implements StoppableEventInterface +abstract class AbstractStoppableEvent implements StoppableEventInterface { private bool $propagationStopped = false; diff --git a/src/ListenerProvider.php b/src/ListenerProvider.php index c79ec87..6aaa0c3 100644 --- a/src/ListenerProvider.php +++ b/src/ListenerProvider.php @@ -14,14 +14,18 @@ */ class ListenerProvider implements ListenerProviderInterface { - /** @var array> */ + /** + * @var array, array> + * @template T of object + */ private array $listeners = []; /** * Registers a listener for a specific event class. * - * @param string $eventClass The fully qualified class name of the event - * @param callable $listener The listener callback that will handle the event + * @template T of object + * @param class-string $eventClass The fully qualified class name of the event + * @param callable(T): void $listener The listener callback that will handle the event */ public function addListener(string $eventClass, callable $listener): void { @@ -31,8 +35,9 @@ public function addListener(string $eventClass, callable $listener): void /** * Gets all listeners registered for the given event. * - * @param object $event The event to get listeners for - * @return iterable The registered listeners + * @template T of object + * @param T $event The event to get listeners for + * @return array The registered listeners */ public function getListenersForEvent(object $event): iterable { diff --git a/tests/AsyncEventDispatcherTest.php b/tests/AsyncEventDispatcherTest.php index 96a4e3f..15d9f2a 100644 --- a/tests/AsyncEventDispatcherTest.php +++ b/tests/AsyncEventDispatcherTest.php @@ -4,12 +4,21 @@ namespace ArchiPro\EventDispatcher\Tests; +use Amp\CancelledException; + +use function Amp\delay; + +use Amp\TimeoutCancellation; use ArchiPro\EventDispatcher\AsyncEventDispatcher; +use ArchiPro\EventDispatcher\Event\AbstractStoppableEvent; use ArchiPro\EventDispatcher\ListenerProvider; use ArchiPro\EventDispatcher\Tests\Fixture\TestEvent; +use Exception; use PHPUnit\Framework\TestCase; use Revolt\EventLoop; +use Throwable; + /** * Test cases for AsyncEventDispatcher. * @@ -33,7 +42,7 @@ protected function setUp(): void $this->listenerProvider = new ListenerProvider(); $this->dispatcher = new AsyncEventDispatcher($this->listenerProvider); - EventLoop::setErrorHandler(function (\Throwable $err) { + EventLoop::setErrorHandler(function (Throwable $err) { throw $err; }); @@ -48,24 +57,24 @@ public function testDispatchEventToMultipleListeners(): void $completed = false; $this->listenerProvider->addListener(TestEvent::class, function (TestEvent $event) use (&$results) { - EventLoop::delay(0.1, fn () => null); + delay(0.1); $results[] = 'listener1: ' . $event->data; }); $this->listenerProvider->addListener(TestEvent::class, function (TestEvent $event) use (&$results, &$completed) { - EventLoop::delay(0.05, fn () => null); + delay(0.05); $results[] = 'listener2: ' . $event->data; $completed = true; }); $event = new TestEvent('test data'); - $this->dispatcher->dispatch($event); + $futureEvent = $this->dispatcher->dispatch($event); // Verify immediate return $this->assertEmpty($results); // Run the event loop until listeners complete - EventLoop::run(); + $futureEvent->await(); $this->assertTrue($completed); $this->assertCount(2, $results); @@ -90,8 +99,7 @@ public function testSynchronousStoppableEvent(): void }); $event = new TestEvent('test data'); - $this->dispatcher->dispatch($event); - EventLoop::run(); + $this->dispatcher->dispatch($event)->await(); $this->assertCount(1, $results); $this->assertEquals(['listener1'], $results); @@ -105,7 +113,7 @@ public function testNoListenersForEvent(): void $event = new TestEvent('test data'); $dispatchedEvent = $this->dispatcher->dispatch($event); - $this->assertSame($event, $dispatchedEvent); + $this->assertSame($event, $dispatchedEvent->await()); } /** @@ -122,13 +130,82 @@ public function testDispatchesNonStoppableEvents(): void }; $this->listenerProvider->addListener(get_class($event), $listener); - $this->dispatcher->dispatch($event); + $futureEvent = $this->dispatcher->dispatch($event); $this->assertFalse($event->called, 'Listener should not have been called right away'); - EventLoop::run(); + $futureEvent->await(); $this->assertTrue($event->called, 'Listener should have been called for non-stoppable event'); } + public function testDispatchesFailureInOneListenerDoesNotAffectOthers(): void + { + $event = new class () { + public bool $calledOnce = false; + public bool $calledTwice = false; + }; + + $this->listenerProvider->addListener(get_class($event), function ($event) { + $event->calledOnce = true; + throw new Exception('Test exception'); + }); + + $this->listenerProvider->addListener(get_class($event), function ($event) { + $event->calledTwice = true; + throw new Exception('Test exception'); + }); + + $futureEvent = $this->dispatcher->dispatch($event); + + $futureEvent = $futureEvent->await(); + + $this->assertTrue( + $futureEvent->calledOnce, + 'The first listener should have been called' + ); + $this->assertTrue( + $futureEvent->calledTwice, + 'The second listener should have been called despite the failure of the first listener' + ); + } + + public function testCancellationOfStoppableEvent(): void + { + $event = new class () extends AbstractStoppableEvent { + public bool $called = false; + }; + + $this->listenerProvider->addListener(get_class($event), function ($event) { + // Simulate a long-running operation + delay(0.1); + $event->called = true; + }); + + $cancellation = new TimeoutCancellation(0.05); + + $this->expectException(CancelledException::class, 'Our listener should have been cancelled'); + + $this->dispatcher->dispatch($event, $cancellation)->await(); + } + + public function testCancellationOfNonStoppableEvent(): void + { + $event = new class () { + public bool $called = false; + }; + + $this->listenerProvider->addListener(get_class($event), function ($event) { + // Simulate a long-running operation + delay(0.1); + $event->called = true; + }); + + $cancellation = new TimeoutCancellation(0.05); + + $this->expectException(CancelledException::class, 'Our listener should have been cancelled'); + + $this->dispatcher->dispatch($event, $cancellation)->await(); + } + } diff --git a/tests/Fixture/TestEvent.php b/tests/Fixture/TestEvent.php index 9b5110a..3ab16cd 100644 --- a/tests/Fixture/TestEvent.php +++ b/tests/Fixture/TestEvent.php @@ -4,7 +4,7 @@ namespace ArchiPro\EventDispatcher\Tests\Fixture; -use ArchiPro\EventDispatcher\Event\AbstractEvent; +use ArchiPro\EventDispatcher\Event\AbstractStoppableEvent; /** * Simple event implementation for testing purposes. @@ -12,7 +12,7 @@ * Contains a single data property that can be used to verify event handling * in test cases. */ -class TestEvent extends AbstractEvent +class TestEvent extends AbstractStoppableEvent { /** * @param string $data Test data to be carried by the event From 6dd1ee725046c57c67118058da848002f0e46750 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 13 Nov 2024 13:36:38 +1300 Subject: [PATCH 13/15] fix linting issue --- examples/basic-usage.php | 4 ++-- phpstan.neon | 3 +-- src/ListenerProvider.php | 3 +-- tests/AsyncEventDispatcherTest.php | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/basic-usage.php b/examples/basic-usage.php index 0fc51e9..9e845d1 100644 --- a/examples/basic-usage.php +++ b/examples/basic-usage.php @@ -3,12 +3,12 @@ require __DIR__ . '/../vendor/autoload.php'; use ArchiPro\EventDispatcher\AsyncEventDispatcher; -use ArchiPro\EventDispatcher\Event\AbstractEvent; +use ArchiPro\EventDispatcher\Event\AbstractStoppableEvent; use ArchiPro\EventDispatcher\ListenerProvider; use Revolt\EventLoop; // Create a custom event -class UserCreatedEvent extends AbstractEvent +class UserCreatedEvent extends AbstractStoppableEvent { public function __construct( public readonly string $userId, diff --git a/phpstan.neon b/phpstan.neon index 306d3c0..5c602a2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,5 +4,4 @@ parameters: - src - tests - examples - tmpDir: phpstan.cache - checkMissingIterableValueType: false \ No newline at end of file + tmpDir: phpstan.cache \ No newline at end of file diff --git a/src/ListenerProvider.php b/src/ListenerProvider.php index 6aaa0c3..69863f6 100644 --- a/src/ListenerProvider.php +++ b/src/ListenerProvider.php @@ -15,8 +15,7 @@ class ListenerProvider implements ListenerProviderInterface { /** - * @var array, array> - * @template T of object + * @var array> */ private array $listeners = []; diff --git a/tests/AsyncEventDispatcherTest.php b/tests/AsyncEventDispatcherTest.php index 15d9f2a..6e4b7a8 100644 --- a/tests/AsyncEventDispatcherTest.php +++ b/tests/AsyncEventDispatcherTest.php @@ -184,7 +184,7 @@ public function testCancellationOfStoppableEvent(): void $cancellation = new TimeoutCancellation(0.05); - $this->expectException(CancelledException::class, 'Our listener should have been cancelled'); + $this->expectException(CancelledException::class); $this->dispatcher->dispatch($event, $cancellation)->await(); } @@ -203,7 +203,7 @@ public function testCancellationOfNonStoppableEvent(): void $cancellation = new TimeoutCancellation(0.05); - $this->expectException(CancelledException::class, 'Our listener should have been cancelled'); + $this->expectException(CancelledException::class); $this->dispatcher->dispatch($event, $cancellation)->await(); } From b1b7185bf14f73a4cecb46e90b336f5607248bcf Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 13 Nov 2024 13:42:53 +1300 Subject: [PATCH 14/15] set license to BSD-3 --- LICENSE.md | 29 +++++++++++++++++++++++++++++ composer.json | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..164725a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2024, ArchiPro +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/composer.json b/composer.json index fdaae84..7ccaa42 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "@test" ] }, - "license": "MIT", + "license": "BSD-3-Clause", "provide": { "psr/event-dispatcher-implementation": "1.0" } From a191094c3f98c7533c3dc743a2f9faf011fe0f0f Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 13 Nov 2024 13:53:12 +1300 Subject: [PATCH 15/15] Update example --- examples/basic-usage.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/basic-usage.php b/examples/basic-usage.php index 9e845d1..31de479 100644 --- a/examples/basic-usage.php +++ b/examples/basic-usage.php @@ -2,6 +2,7 @@ require __DIR__ . '/../vendor/autoload.php'; +use Amp\TimeoutCancellation; use ArchiPro\EventDispatcher\AsyncEventDispatcher; use ArchiPro\EventDispatcher\Event\AbstractStoppableEvent; use ArchiPro\EventDispatcher\ListenerProvider; @@ -47,5 +48,15 @@ function () use ($event) { $event = new UserCreatedEvent('123', 'user@example.com'); $dispatcher->dispatch($event); -// Run the event loop +// Run the event loop to process all events +EventLoop::run(); + +// Wait for the event to finish right away +$event = new UserCreatedEvent('456', 'user@example.com'); +$future = $dispatcher->dispatch($event); +$updatedEvent = $future->await(); + +// Make an event cancellable +$event = new UserCreatedEvent('789', 'user@example.com'); +$future = $dispatcher->dispatch($event, new TimeoutCancellation(30)); EventLoop::run();