diff --git a/composer.json b/composer.json index 4e0708f..9aa5a7c 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "rector/rector": "^0.15.17", "psr/container": "^1.0", "psr/event-dispatcher": "^1.0", - "fig/event-dispatcher-util": "^1.0", + "fig/event-dispatcher-util": "^1.3", "crell/tukio": "^1.0", "inpsyde/object-hooks-remover": "^0.1", "italystrap/config": "^2.2", diff --git a/src/Dispatcher.php b/src/Dispatcher.php index 93c1b00..f84acb1 100644 --- a/src/Dispatcher.php +++ b/src/Dispatcher.php @@ -4,12 +4,16 @@ namespace ItalyStrap\Event; -use Psr\EventDispatcher\{ListenerProviderInterface, StoppableEventInterface}; +use Psr\EventDispatcher\{ + EventDispatcherInterface, + ListenerProviderInterface, + StoppableEventInterface +}; /** * @psalm-api */ -class Dispatcher implements \Psr\EventDispatcher\EventDispatcherInterface +class Dispatcher implements EventDispatcherInterface { private ListenerProviderInterface $listenerProvider; private StateInterface $state; diff --git a/src/EventSubscription.php b/src/EventSubscription.php index d252c84..1c11730 100644 --- a/src/EventSubscription.php +++ b/src/EventSubscription.php @@ -17,7 +17,7 @@ class EventSubscription * @param int $acceptedArgs */ public function __construct( - $callback, + callable $callback, int $priority = 10, int $acceptedArgs = 3 ) { diff --git a/src/GlobalOrderedListenerProvider.php b/src/GlobalOrderedListenerProvider.php index 664bdb4..82896f1 100644 --- a/src/GlobalOrderedListenerProvider.php +++ b/src/GlobalOrderedListenerProvider.php @@ -64,7 +64,7 @@ public function getListenersForEvent(object $event): iterable /** * \WP_Hook::callbacks is a multidimensional array * with priority as the first dimension and - * the callback as the second dimension. + * the callback name as the second dimension. * Example: * [ * 10 => [ // $priority diff --git a/src/ListenerRegistryTrait.php b/src/ListenerRegistryTrait.php index fdcd167..d00fe62 100644 --- a/src/ListenerRegistryTrait.php +++ b/src/ListenerRegistryTrait.php @@ -6,6 +6,24 @@ trait ListenerRegistryTrait { + /** + * Right now is only an experimental method, an idea to fetch $eventName + * from the listener first argument using reflection. + * I make this private for now to avoid to deprecated it in the future. + */ + private function addListenerFromCallable( + callable $listener, + string $eventName = null, + int $priority = 10, + int $accepted_args = 3 + ): bool { + if (null === $eventName) { + return false; + } + + return add_filter($eventName, $listener, $priority, $accepted_args); + } + public function addListener( string $eventName, callable $listener, diff --git a/src/Module.php b/src/Module.php new file mode 100644 index 0000000..0697fd3 --- /dev/null +++ b/src/Module.php @@ -0,0 +1,37 @@ + [ + // Global + EventDispatcherInterface::class => EventDispatcher::class, + SubscriberRegisterInterface::class => SubscriberRegister::class, + // PSR-14 + \Psr\EventDispatcher\EventDispatcherInterface::class => Dispatcher::class, + ListenerProviderInterface::class => GlobalOrderedListenerProvider::class, + ListenerRegistryInterface::class => GlobalOrderedListenerProvider::class, + StateInterface::class => GlobalState::class, + ], + 'sharing' => [ + EventDispatcher::class, + SubscriberRegister::class, + // PSR-14 + Dispatcher::class, + GlobalOrderedListenerProvider::class, + GlobalState::class, + ], + ]; + } +} diff --git a/src/ParameterKeys.php b/src/ParameterKeys.php deleted file mode 100644 index c0bd246..0000000 --- a/src/ParameterKeys.php +++ /dev/null @@ -1,24 +0,0 @@ -event_manager = $event_manager; + $this->subscriberRegister = $subscriberRegister; $this->config = $config; } @@ -47,7 +47,7 @@ public function name(): string */ public function execute(AurynConfigInterface $application): void { - $application->walk($this->name(), [$this, 'walk']); + $application->walk($this->name(), $this); } /** @@ -57,7 +57,7 @@ public function execute(AurynConfigInterface $application): void * @throws ConfigException * @throws InjectionException */ - public function walk(string $class, $index_or_optionName, Injector $injector): void + public function __invoke(string $class, $index_or_optionName, Injector $injector): void { if (\is_string($index_or_optionName) && empty($this->config->get($index_or_optionName, false))) { @@ -66,6 +66,6 @@ public function walk(string $class, $index_or_optionName, Injector $injector): v /** @var SubscriberInterface $subscriber */ $subscriber = $injector->share($class)->make($class); - $this->event_manager->addSubscriber($subscriber); + $this->subscriberRegister->addSubscriber($subscriber); } } diff --git a/tests/_data/experiment/PsrDispatcher/DebugDispatcher.php b/tests/_data/experiment/PsrDispatcher/DebugDispatcher.php deleted file mode 100644 index 6981c17..0000000 --- a/tests/_data/experiment/PsrDispatcher/DebugDispatcher.php +++ /dev/null @@ -1,48 +0,0 @@ -dispatcher = $dispatcher; - $this->logger = $logger; - } - - public function dispatch(object $event) - { - $this->logger->debug(self::M_DEBUG, ['type' => get_class($event), 'event' => $event]); - return $this->dispatcher->dispatch($event); - } -} diff --git a/tests/integration/ImplementationTest.php b/tests/integration/ImplementationTest.php index 1f0db21..313d4a0 100644 --- a/tests/integration/ImplementationTest.php +++ b/tests/integration/ImplementationTest.php @@ -5,6 +5,9 @@ namespace ItalyStrap\Tests\Integration; use Crell\Tukio\OrderedListenerProvider; +use Fig\EventDispatcher\AggregateProvider; +use Fig\EventDispatcher\TaggedProviderTrait; +use ItalyStrap\Event\GlobalOrderedListenerProvider; use ItalyStrap\Tests\EventForRenderer; use ItalyStrap\Tests\IntegrationTestCase; use ItalyStrap\Tests\RendererAsEvent; @@ -492,7 +495,7 @@ public function testAddAndRemoveListenerForRendererWithEvent(): void public function testWordPressListenerWithExternalPackage(): void { $listenerProvider = new class extends OrderedListenerProvider implements ListenerProviderInterface { - public function addListener( + public function addListenerFromCallable( callable $listener, ?int $priority = null, ?string $id = null, @@ -517,7 +520,7 @@ public function addListener( $event->rendered = 'Hello there'; }; - $listenerProvider->addListener($listener, null, null, EventForRenderer::class); + $listenerProvider->addListenerFromCallable($listener, null, null, EventForRenderer::class); Assert::assertSame('Hello there', $dispatcher->dispatch($event)->rendered); @@ -528,6 +531,147 @@ public function addListener( $event = new EventForRenderer(); $value = \apply_filters(EventForRenderer::class, $event); Assert::assertSame('Hello there', $event->rendered); + // Because the $listener callback does not return a value the $value will be null Assert::assertNull($value, 'The return value of the filter should be null'); } + + /** + * If you want to use string event name you can still do it with `addListener()` method + * because the `addListener()` method use the `add_filter()` function to register the listener, + * but pay attention, if you want to dispatch the event you need to use one of the WordPress Hooks API + * `do_action()` or `apply_filters()`. + * + * The simple explanation is that the `dispatch()` method is only aware of the event as object + * and under the hood when loop the stack of listeners only a listener that match + * the event object name will be executed. + */ + public function testAddListenerForRendererWithEventString(): void + { + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $listener = function (object $event) { + $event->rendered = 'Hello there'; + }; + + $listenerProvider->addListener('event_name', $listener); + + $event = new EventForRenderer(); + \do_action('event_name', $event); + + Assert::assertSame('Hello there', $event->rendered); + + $dispatcher->dispatch($event); + // As you can see the `dispatch()` method does not change the event. + Assert::assertSame('Hello there', $event->rendered); + } + + /** + * Now this example shows the possibility to use an alias for the event name, so instead of using + * the object event name you can use a string event name and bind it to the object event name. + * Right now is still experimental, need to be tested more. + * + * But this could be dangerous because if you bind for example an event name to a string that is already + * used by classic `do_action` or `apply_filters()` you could have some unexpected behaviour, just to name a few: + * - the_title + * - the_content + * - the_excerpt + * and so on. + */ + public function testAddListenerForRendererWithEventStringAsAlias(): void + { + $listenerProvider = new class implements ListenerProviderInterface { + private ListenerProviderInterface $listenerProvider; + private array $aliases = []; + + public function __construct() + { + $this->listenerProvider = new GlobalOrderedListenerProvider(); + } + + public function alias(string $alias, string $eventName): void + { + $this->aliases[$eventName] = $alias; + } + + public function addListener(string $eventName, callable $listener, int $priority = 10): bool + { + return $this->listenerProvider->addListener($eventName, $listener, $priority); + } + + public function getListenersForEvent(object $event): iterable + { + global $wp_filter; + $callbacks = []; + $eventName = \get_class($event); + $eventName = $this->aliases[$eventName] ?? $eventName; + + if (!\array_key_exists($eventName, $wp_filter)) { + return $callbacks; + } + + if (!$wp_filter[$eventName] instanceof \WP_Hook) { + return $callbacks; + } + + foreach ($wp_filter[$eventName]->callbacks as $callbacks) { + foreach ($callbacks as $callback) { + yield $callback['function']; + } + } + } + }; + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $listener = function (object $event) { + $event->rendered = 'Hello there'; + }; + + $listenerProvider->alias('event_name', EventForRenderer::class); + $listenerProvider->addListener('event_name', $listener); + + $event = new EventForRenderer(); + Assert::assertSame('Hello World', $event->rendered); + + // This event name is aliased so calling the `do_action()` with alias name will change the event + \do_action('event_name', $event); + // As you can see the event is changed + Assert::assertSame('Hello there', $event->rendered); + + // Revert the event name to the original + $event = new EventForRenderer(); + Assert::assertSame('Hello World', $event->rendered); + + // Because is aliased now the `dispatch()` method is triggered. + $dispatcher->dispatch($event); + // And the event is changed + Assert::assertSame('Hello there', $event->rendered); + + /** + * Just a reminder, because we add an alias name to `add_filter()` + * `do_action()` and `apply_filters()` know only the alias name and not the original object event name. + */ + } + + public function testAggregateProvider(): void + { + $aggregateProvider = new AggregateProvider(); + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + $aggregateProvider->addProvider($listenerProvider); + $dispatcher = new \ItalyStrap\Event\Dispatcher($aggregateProvider); + + $event = new EventForRenderer(); + Assert::assertSame('Hello World', $event->rendered); + + $listener = function (object $event) { + $event->rendered = 'Hello there'; + }; + + $listenerProvider->addListener(EventForRenderer::class, $listener); + + $dispatcher->dispatch($event); + Assert::assertSame('Hello there', $event->rendered); + } } diff --git a/tests/src/EventSubscriptionTrait.php b/tests/src/EventSubscriptionTrait.php index 7958686..16722ef 100644 --- a/tests/src/EventSubscriptionTrait.php +++ b/tests/src/EventSubscriptionTrait.php @@ -18,6 +18,8 @@ trait EventSubscriptionTrait public function testShouldBeInstantiable() { + $this->callback = function () { + }; $sut = $this->makeInstance(); $this->assertInstanceOf(EventSubscription::class, $sut); } diff --git a/tests/unit/DebugDispatcherTest.php b/tests/unit/DebugDispatcherTest.php index 8910859..3148300 100644 --- a/tests/unit/DebugDispatcherTest.php +++ b/tests/unit/DebugDispatcherTest.php @@ -4,7 +4,7 @@ namespace ItalyStrap\Tests\Unit; -use ItalyStrap\PsrDispatcher\DebugDispatcher; +use ItalyStrap\Event\DebugDispatcher; use ItalyStrap\Tests\UnitTestCase; use Prophecy\Argument; use Psr\EventDispatcher\EventDispatcherInterface; diff --git a/tests/unit/SubscribersConfigExtensionTest.php b/tests/unit/SubscribersConfigExtensionTest.php index 039b9d6..68022da 100644 --- a/tests/unit/SubscribersConfigExtensionTest.php +++ b/tests/unit/SubscribersConfigExtensionTest.php @@ -48,7 +48,7 @@ public function callbackShouldSubscribeListenersWithIndexedArray() ->shouldBeCalled(); $sut = $this->makeInstance(); - $sut->walk(SubscriberMock::class, 0, $this->makeFakeInjector()); + $sut(SubscriberMock::class, 0, $this->makeFakeInjector()); } /** @@ -74,7 +74,7 @@ public function callbackShouldSubscribeListenersFormAssociativeArrayWithTrueOpti ->shouldBeCalled(); $sut = $this->makeInstance(); - $sut->walk(SubscriberMock::class, $key, $this->makeFakeInjector()); + $sut(SubscriberMock::class, $key, $this->makeFakeInjector()); } /** @@ -100,7 +100,7 @@ public function callbackShouldNotSubscribeListenersFromAssociativeArrayWithFalse ->shouldNotBeCalled(); $sut = $this->makeInstance(); - $sut->walk(SubscriberMock::class, $key, $this->makeFakeInjector()); + $sut(SubscriberMock::class, $key, $this->makeFakeInjector()); } /**