From 9cd723ebcbf5adf35fa958d21cd5526b6f10338b Mon Sep 17 00:00:00 2001 From: Joshua Estes Date: Wed, 27 Dec 2023 14:13:13 -0500 Subject: [PATCH] [EventDispatcher] Adding a few new things and working on docs (#202) ## Description ## Checklist - [x] Updated CHANGELOG files - [x] Updated Documentation - [x] Unit Tests Created - [x] php-cs-fixer --- CHANGELOG.md | 1 + Makefile | 7 +- docs/components/event-dispatcher/index.md | 100 ++++++++++++++++++ .../AbstractStoppableEvent.php | 15 +++ .../EventDispatcher/EventDispatcher.php | 15 ++- .../EventSubscriberInterface.php | 4 + .../EventDispatcher/ListenerInterface.php | 10 -- .../EventDispatcher/ListenerProvider.php | 11 ++ .../EventDispatcher/StoppableEventTrait.php | 26 +++++ .../Tests/AbstractStoppableEventTest.php | 40 +++++++ .../Tests/EventDispatcherTest.php | 44 ++++++++ 11 files changed, 259 insertions(+), 14 deletions(-) create mode 100644 src/SonsOfPHP/Component/EventDispatcher/AbstractStoppableEvent.php delete mode 100644 src/SonsOfPHP/Component/EventDispatcher/ListenerInterface.php create mode 100644 src/SonsOfPHP/Component/EventDispatcher/StoppableEventTrait.php create mode 100644 src/SonsOfPHP/Component/EventDispatcher/Tests/AbstractStoppableEventTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 691a6733..9c140b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ To get the diff between two versions, go to https://github.com/SonsOfPHP/sonsofp * [PR #182](https://github.com/SonsOfPHP/sonsofphp/pull/182) [Container] New Component (PSR-11) * [PR #187](https://github.com/SonsOfPHP/sonsofphp/pull/187) [HttpHandler] New Component (PSR-15) and Contract * [PR #190](https://github.com/SonsOfPHP/sonsofphp/pull/190) [Mailer] New Component and Contract +* [PR #202](https://github.com/SonsOfPHP/sonsofphp/pull/202) [EventDispatcher] Added Stoppable Event Code ## [0.3.8] diff --git a/Makefile b/Makefile index 8a0c2c7f..629c3085 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,9 @@ test-cookie: phpunit test-cqrs: PHPUNIT_TESTSUITE=cqrs test-cqrs: phpunit +test-event-dispatcher: PHPUNIT_TESTSUITE=event-dispatcher +test-event-dispatcher: phpunit + test-http-factory: PHPUNIT_TESTSUITE=http-factory test-http-factory: phpunit @@ -125,8 +128,8 @@ coverage-cookie: coverage coverage-cqrs: PHPUNIT_TESTSUITE=cqrs coverage-cqrs: coverage -coverage-event-dispatcher: - XDEBUG_MODE=coverage $(PHP) -dxdebug.mode=coverage $(PHPUNIT) --testsuite event-dispatcher --coverage-html $(COVERAGE_DIR) +coverage-event-dispatcher: PHPUNIT_TESTSUITE=event-dispatcher +coverage-event-dispatcher: coverage coverage-event-sourcing: XDEBUG_MODE=coverage $(PHP) -dxdebug.mode=coverage $(PHPUNIT) --testsuite event-sourcing --coverage-html $(COVERAGE_DIR) diff --git a/docs/components/event-dispatcher/index.md b/docs/components/event-dispatcher/index.md index 138b87e5..46493a92 100644 --- a/docs/components/event-dispatcher/index.md +++ b/docs/components/event-dispatcher/index.md @@ -49,9 +49,109 @@ Must implement `EventSubscriberInterface`. ### Listener Priorities ```php +addListener('event.name', function () {}, $priority); ``` The priority will default to `0`. Lower numbers are higher priority. Higher numbers will be handled later. For example, a listener with a priority of `-1` will be handled before a listener of priority `1`. + + +### Stoppable Events + +If your code extends the `AbstractStoppableEvent` and within your listener or +subscriber code, you execute `$this->stopPropagation();` and it will return the +event and no more listeners or subscribers will handle that event. + +```php +stopPropagation(); + // ... + } +} +``` + +You can also create Stoppable Events by using the `StoppableEventInterface` and +`StoppableEventTrait`. + +```php + 'onCreated'; + yield OrderUpdated::class => ['onUpdated', 100]; + yield OrderDeleted::class => [['onDeleted', 100], ['doFirstOnDeleted', -100]]; + + // OR like this + return [ + OrderCreated::class => 'onCreated', + OrderUpdated::class => ['onUpdated', 100], + OrderDeleted::class => [['onDeleted', 100], ['doFirstOnDeleted', -100]], + ]; + } +} +``` diff --git a/src/SonsOfPHP/Component/EventDispatcher/AbstractStoppableEvent.php b/src/SonsOfPHP/Component/EventDispatcher/AbstractStoppableEvent.php new file mode 100644 index 00000000..5e3845b9 --- /dev/null +++ b/src/SonsOfPHP/Component/EventDispatcher/AbstractStoppableEvent.php @@ -0,0 +1,15 @@ + + */ +abstract class AbstractStoppableEvent implements StoppableEventInterface +{ + use StoppableEventTrait; +} diff --git a/src/SonsOfPHP/Component/EventDispatcher/EventDispatcher.php b/src/SonsOfPHP/Component/EventDispatcher/EventDispatcher.php index 963e89ac..932e4e00 100644 --- a/src/SonsOfPHP/Component/EventDispatcher/EventDispatcher.php +++ b/src/SonsOfPHP/Component/EventDispatcher/EventDispatcher.php @@ -18,7 +18,10 @@ public function __construct( ) {} /** - * @return object + * {@inheritdoc} + * + * @param string|null $eventName + * Is the event name is null, is will use the event's classname as the Event Name */ public function dispatch(object $event, string $eventName = null): object { @@ -35,11 +38,19 @@ public function dispatch(object $event, string $eventName = null): object return $event; } - public function addListener(string $eventName, callable|array $listener, int $priority = 0): void + /** + */ + public function addListener(string|object $eventName, callable|array $listener, int $priority = 0): void { + if (is_object($eventName)) { + $eventName = $eventName::class; + } + $this->provider->add($eventName, $listener, $priority); } + /** + */ public function addSubscriber(EventSubscriberInterface $subscriber): void { $this->provider->addSubscriber($subscriber); diff --git a/src/SonsOfPHP/Component/EventDispatcher/EventSubscriberInterface.php b/src/SonsOfPHP/Component/EventDispatcher/EventSubscriberInterface.php index f7142e06..7caef4ee 100644 --- a/src/SonsOfPHP/Component/EventDispatcher/EventSubscriberInterface.php +++ b/src/SonsOfPHP/Component/EventDispatcher/EventSubscriberInterface.php @@ -10,6 +10,10 @@ interface EventSubscriberInterface { /** + * Returns an iterable with one or more of the following + * - ['eventName' => 'methodName'], ... + * - ['eventName' => ['methodName', $priority], ...], ... + * - ['eventName' => [['methodName', $priority], ['methodName']], ...], ... */ public static function getSubscribedEvents(); } diff --git a/src/SonsOfPHP/Component/EventDispatcher/ListenerInterface.php b/src/SonsOfPHP/Component/EventDispatcher/ListenerInterface.php deleted file mode 100644 index 72ec217a..00000000 --- a/src/SonsOfPHP/Component/EventDispatcher/ListenerInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -getListenersForEventName($event::class); } + /** + */ public function add(string $eventName, callable|array $listener, int $priority = 0): void { $this->listeners[$eventName][$priority][] = $listener; unset($this->sorted[$eventName]); } + /** + */ public function addSubscriber(EventSubscriberInterface $subscriber): void { foreach ($subscriber::getSubscribedEvents() as $eventName => $params) { @@ -48,6 +55,8 @@ public function addSubscriber(EventSubscriberInterface $subscriber): void } } + /** + */ public function getListenersForEventName(string $eventName): iterable { if (!\array_key_exists($eventName, $this->listeners)) { @@ -61,6 +70,8 @@ public function getListenersForEventName(string $eventName): iterable return $this->sorted[$eventName]; } + /** + */ private function sortListeners(string $eventName): void { ksort($this->listeners[$eventName]); diff --git a/src/SonsOfPHP/Component/EventDispatcher/StoppableEventTrait.php b/src/SonsOfPHP/Component/EventDispatcher/StoppableEventTrait.php new file mode 100644 index 00000000..8fb1faf8 --- /dev/null +++ b/src/SonsOfPHP/Component/EventDispatcher/StoppableEventTrait.php @@ -0,0 +1,26 @@ + + */ +trait StoppableEventTrait +{ + private bool $isStopped = false; + + public function isPropagationStopped(): bool + { + return $this->isStopped; + } + + /** + * Makes `isPropagationStopped` return true + */ + public function stopPropagation(): void + { + $this->isStopped = true; + } +} diff --git a/src/SonsOfPHP/Component/EventDispatcher/Tests/AbstractStoppableEventTest.php b/src/SonsOfPHP/Component/EventDispatcher/Tests/AbstractStoppableEventTest.php new file mode 100644 index 00000000..812794ce --- /dev/null +++ b/src/SonsOfPHP/Component/EventDispatcher/Tests/AbstractStoppableEventTest.php @@ -0,0 +1,40 @@ +assertInstanceOf(StoppableEventInterface::class, $event); + } + + /** + * @covers ::isPropagationStopped + * @covers ::stopPropagation + */ + public function testItCanStopPropagation(): void + { + $event = new class () extends AbstractStoppableEvent {}; + $this->assertFalse($event->isPropagationStopped()); + + $event->stopPropagation(); + $this->assertTrue($event->isPropagationStopped()); + } +} diff --git a/src/SonsOfPHP/Component/EventDispatcher/Tests/EventDispatcherTest.php b/src/SonsOfPHP/Component/EventDispatcher/Tests/EventDispatcherTest.php index 9a760b6a..69534fe2 100644 --- a/src/SonsOfPHP/Component/EventDispatcher/Tests/EventDispatcherTest.php +++ b/src/SonsOfPHP/Component/EventDispatcher/Tests/EventDispatcherTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; +use SonsOfPHP\Component\EventDispatcher\AbstractStoppableEvent; use SonsOfPHP\Component\EventDispatcher\EventDispatcher; use SonsOfPHP\Component\EventDispatcher\EventSubscriberInterface; use SonsOfPHP\Component\EventDispatcher\ListenerProvider; @@ -15,6 +16,7 @@ * * @uses \SonsOfPHP\Component\EventDispatcher\EventDispatcher * @uses \SonsOfPHP\Component\EventDispatcher\ListenerProvider + * @uses \SonsOfPHP\Component\EventDispatcher\StoppableEventTrait */ final class EventDispatcherTest extends TestCase { @@ -28,6 +30,35 @@ public function testItHasTheCorrectInterface(): void $this->assertInstanceOf(EventDispatcherInterface::class, $dispatcher); // @phpstan-ignore-line } + /** + * @covers ::dispatch + */ + public function testDispatch(): void + { + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('stdClass', function ($event): void {}); + + $event = new \stdClass(); + $this->assertSame($event, $dispatcher->dispatch($event)); + } + + /** + * @covers ::dispatch + */ + public function testDispatchWithStoppedEvent(): void + { + $event = new class () extends AbstractStoppableEvent {}; + + $dispatcher = new EventDispatcher(); + $dispatcher->addListener($event, function ($event): void { + throw new \RuntimeException('This should never run'); + }); + + $event->stopPropagation(); + + $this->assertSame($event, $dispatcher->dispatch($event)); + } + /** * @covers ::dispatch */ @@ -52,6 +83,19 @@ public function testItCanAddEventListener(): void $dispatcher->addListener('stdClass', function (): void {}); } + /** + * @covers ::addListener + */ + public function testAddListenerWithObject(): void + { + $provider = $this->createMock(ListenerProvider::class); + $provider->expects($this->once())->method('add')->with($this->identicalTo('stdClass')); + + $dispatcher = new EventDispatcher($provider); + + $dispatcher->addListener(new \stdClass(), function (): void {}); + } + /** * @covers ::addSubscriber */