From 99c6446395f7ee16d19fbad4616a6b1ef094d95c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 2 Apr 2019 09:54:13 -0500 Subject: [PATCH 01/26] feat: StoppableEventInterface implementation Since v3 still targets PHP versions prior to PHP 7.2, this patch provides a forwards-compatibility shim for the PSR-14 `StoppableEventInterface` via an additional, package-specific version that is now also implemented by default in the `Event` class. --- src/Event.php | 20 ++++++++++++++------ src/EventInterface.php | 2 ++ src/StoppableEventInterface.php | 22 ++++++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 src/StoppableEventInterface.php diff --git a/src/Event.php b/src/Event.php index e92865f..089bd66 100644 --- a/src/Event.php +++ b/src/Event.php @@ -1,10 +1,8 @@ stopPropagation; } + + /** + * {@inheritDoc} + */ + public function isPropagationStopped() + { + return $this->stopPropagation; + } } diff --git a/src/EventInterface.php b/src/EventInterface.php index 76f2d48..11c3a5b 100644 --- a/src/EventInterface.php +++ b/src/EventInterface.php @@ -90,6 +90,8 @@ public function stopPropagation($flag = true); /** * Has this event indicated event propagation should stop? * + * @deprecated Implement StoppableEventInterface instead, to make your + * application forwards-compatible with PSR-14 and zend-eventmanager v4. * @return bool */ public function propagationIsStopped(); diff --git a/src/StoppableEventInterface.php b/src/StoppableEventInterface.php new file mode 100644 index 0000000..3d52a6c --- /dev/null +++ b/src/StoppableEventInterface.php @@ -0,0 +1,22 @@ + Date: Tue, 2 Apr 2019 09:59:58 -0500 Subject: [PATCH 02/26] feat: prefer `isPropagationStopped` over `propagationIsStopped` If the method `isPropagationStopped()` is defined, use it over the `propagationIsStopped()` method. --- src/EventManager.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/EventManager.php b/src/EventManager.php index 9d7c1d1..bb2a6c0 100644 --- a/src/EventManager.php +++ b/src/EventManager.php @@ -314,6 +314,8 @@ protected function triggerListeners(EventInterface $event, callable $callback = // Initial value of stop propagation flag should be false $event->stopPropagation(false); + $stopMethod = $event instanceof StoppableEventInterface ? 'isPropagationStopped' : 'propagationIsStopped'; + // Execute listeners $responses = new ResponseCollection(); foreach ($listOfListenersByPriority as $listOfListeners) { @@ -323,7 +325,7 @@ protected function triggerListeners(EventInterface $event, callable $callback = $responses->push($response); // If the event was asked to stop propagating, do so - if ($event->propagationIsStopped()) { + if ($event->{$stopMethod}()) { $responses->setStopped(true); return $responses; } From 4fbd54a9316daf66e1752c586e19cfb56e08e466 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 2 Apr 2019 10:02:00 -0500 Subject: [PATCH 03/26] feat: proxy to isPropagationStopped Modifies Event::propagationIsStopped such that it now proxies to the isPropagationStopped method, and documents in the deprecation notice that this happens. --- src/Event.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Event.php b/src/Event.php index 089bd66..0291bf3 100644 --- a/src/Event.php +++ b/src/Event.php @@ -191,12 +191,14 @@ public function stopPropagation($flag = true) * Is propagation stopped? * * @deprecated Use isPropagationStopped instead, to make your application - * forwards-compatible with PSR-14 and zend-eventmanager v4. + * forwards-compatible with PSR-14 and zend-eventmanager v4. If you + * plan to override this method, please do so via the `isPropagationStopped` + * method, as this method proxies to that one starting in version 3.3.0. * @return bool */ public function propagationIsStopped() { - return $this->stopPropagation; + return $this->isPropagationStopped(); } /** From 9784e7befdeb4f240e01b797566f99f65034bf0d Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 2 Apr 2019 15:23:53 -0500 Subject: [PATCH 04/26] docs: provide comprehensive checklist for PSR-14 adoption --- TODO-PSR-14.md | 183 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 TODO-PSR-14.md diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md new file mode 100644 index 0000000..9702893 --- /dev/null +++ b/TODO-PSR-14.md @@ -0,0 +1,183 @@ +# TODO for PSR-14 implementation + +## 3.3.0 forwards-compatibility release + +- [ ] `StoppableEventInterface` implementation + - [x] Create a `StoppableEventInterface` + - [x] Make `Event` implement it + - [x] Deprecate `propagationIsStopped()` in both `EventInterface` and `Event` + - [x] Have `Event::propagationIsStopped()` proxy to `Event::isPropagationStopped()` + - [x] Modify `EventManager` internals to use the PSR-14 method if available + - [ ] Mark `StoppableEventInterface` as deprecated +- [ ] Listener provider implementation + - [ ] Create a `ListenerProvider` subnamespace + - [ ] Create a `ListenerProviderInterface` shim + - [ ] Create a `PrioritizedListenerProvider` interface extending the + `ListenerProviderInterface` and defining a + `getListenersForEventByPriority($event, array $identifiers = []) : array` method. + - [ ] Create a `PrioritizedListenerAttachmentInterface`, defining: + - [ ] `attach($event, callable $listener, $priority = 1)` (where `$event` + can be an object or string name) + - [ ] `detach(callable $listener, $event = null, $force = false)` (where `$event` + can be an object or string name and `$force` is boolean) + - [ ] `attachWildcardListener(callable $listener, $priority = 1)` + (`attach('*', $listener, $priority)` will proxy to this method) + - [ ] Create a `PrioritizedListenerProvider` implementation of the above based + on the internals of `EventManager` + - [ ] attachment/detachment + - [ ] getListenersForEvent should take into account event name if an EventInterface + - [ ] getListenersForEvent should also pull wildcard listeners + - [ ] getListenersForEvent should accept an optional second argument, an + array of identifiers. This method will return all listeners in prioritized + order. + - [ ] implement `getListenersForEventByPriority` + - [ ] Create a `PrioritizedIdentifierListenerProvider` that implements + both the `PrioritizedListenerProvider` interface and the + `SharedEventManagerInterface` + - [ ] implement `getListenersForEventByPriority` + - [ ] `SharedEventManager` will extend this class + - [ ] mark as deprecated (will not use this in v4) + - [ ] Create a `PrioritizedAggregateListenerProvider` implementation + - [ ] Accepts a list of `PrioritizedListenerProvider` instances + - [ ] `getListenersByEvent()` will loop through each, in order, calling the + `getListenersForEventByPriority()` method of each, returning the + aggregated listeners in priority order. + - [ ] Create `ListenerSubscriberInterface` + - [ ] `attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1)` + - [ ] `detach(PrioritizedListenerAttachmentInterface $provider)` + - [ ] Create `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait` + - [ ] define a default `detach()` implementation + - [ ] Create `LazyListenerSubscriber` based on `LazyListenerAggregate` + - [ ] Define an alternate LazyListener: + - [ ] `__construct(ContainerInterface $container, string $event = null, int $priority = 1)` + - [ ] implements functionality from both `LazyListener` and `LazyEventListener`, minus passing env to container + - [ ] without an event, can be attached to any provider + - [ ] with an event, can be attached to `LazyListenerSubscriber` + - Constructor aggregates `LazyListener` _instances_ only + - [ ] `attach()` skips any where `getEvent()` returns null +- [ ] Event Dispatcher implementation + - [ ] Create a `PrioritizedListenerProvider` instance in the `EventManger` + constructor, and have the various `attach()`, `detach()`, etc. methods + proxy to it. + - [ ] When triggering listeners, create a `PrioritizedAggregateListenerProvider` + with the composed `PrioritizedListenerProvider` and `SharedListenerProvider` / + `PrioritizedIdentifierListenerProvider` implementations, in that order. + - [ ] Replace logic of `triggerListeners()` to just call + `getListenersForEvent()` on the provider. It can continue to aggregate the + responses in a `ResponseCollection` + - [ ] `triggerListeners()` no longer needs to type-hint its first argument + - [ ] Create a `dispatch()` method + - [ ] Method will act like `triggerEvent()`, except + - [ ] it will return the event itself + - [ ] it will need to validate that it received an object before calling + `triggerListeners` +- [ ] Additional utilities + - [ ] `EventDispatchingInterface` with a `getEventDispatcher()` method + - [ ] Alternate dispatcher implementation, `EventDispatcher` + - [ ] Should accept a listener provider interface to its constructor + - [ ] Should implement `EventDispatcherInterface` via duck-typing: it will + implement a `dispatch()` method only +- [ ] Deprecations + - [ ] `EventInterface` + - [ ] `EventManager` + - [ ] `EventManagerInterface` + - [ ] `EventManagerAwareInterface` + - [ ] `EventManagerAwareTrait` + - [ ] `EventsCapableInterface` (point people to `EventDispatchingInterface`) + - [ ] `SharedEventManager` + - [ ] `SharedEventManagerInterface` + - [ ] `SharedEventsCapableInterface` + - [ ] `ListenerAggregateInterface` (point people to the `PrioritizedListenerAttachmentInterface`) + - [ ] `ListenerAggregateTrait` (point people to `ListenerSubscriberTrait`) + - [ ] `AbstractListenerAggregate` (point people to `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait`) + - [ ] `ResponseCollection` (tell people to aggregate state/results in the event itself) + - [ ] `LazyListener` (point people to `ListenerProvider\LazyListener`) + - [ ] `LazyEventListener` (point people to `ListenerProvider\LazyListener`) + - [ ] `LazyListenerAggregate` (point people to `ListenerProvider\LazyListenerSubscriber`) + - [ ] `FilterChain` and `Filter` subnamespace (this should be done in a separate component) + +## 4.0.0 full release + +- [ ] Removals + - [ ] `EventInterface` + - [ ] `EventManager` + - [ ] `EventManagerInterface` + - [ ] `EventManagerAwareInterface` + - [ ] `EventManagerAwareTrait` + - [ ] `EventsCapableInterface` + - [ ] `SharedEventManager` + - [ ] `SharedEventManagerInterface` + - [ ] `SharedEventsCapableInterface` + - [ ] `ListenerAggregateInterface` + - [ ] `ListenerAggregateTrait` + - [ ] `AbstractListenerAggregate` + - [ ] `ResponseCollection` + - [ ] `LazyListener` + - [ ] `LazyEventListener` + - [ ] `LazyListenerAggregate` + - [ ] `FilterChain` and `Filter` subnamespace + - [ ] `StoppableEventInterface` (will use PSR-14 version) + - [ ] `ListenerProviderInterface` (will use PSR-14 version) + - [ ] `PrioritizedIdentifierListenerProvider` +- Changes + - [ ] `PrioritizedListenerAttachmentInterface` (and implementations) + - [ ] extend PSR-14 `ListenerProviderInterface` + - [ ] add `string` typehint to `$event` in `attach()` and `detach()` + - [ ] add `bool` typehint to `$force` argument of `detach()` + - [ ] `PrioritizedListenerProvider` interface (and implementations) + - [ ] Fulfill PSR-14 `ListenerProviderInterface` + - [ ] remove `$identifiers` argument to getListenersForEventByPriority and getListenersForEvent + - [ ] add `object` typehint to `getListenersForEventByPriority` + - [ ] `EventDispatcher` + - [ ] implement PSR-14 `EventDispatcherInterface` + +## Concerns + +### MVC + +Currently, the MVC relies heavily on: + +- event names (vs types) +- event targets +- event params +- `stopPropagation($flag)` (vs custom stop conditions in events) +- `triggerEventUntil()` (vs custom stop conditions in events) + +We would need to draw attention to usage of methods that are not specific to an +event implementation, and recommend usage of other methods where available. +(We would likely keep the params implementation, however, to allow passing +messages via the event instance(s).) + +Additionally, we will need to have some sort of event hierarchy: + +- a base MVC event from which all others derive. This will be necessary to + ensure that existing code continues to work. +- a BootstrapEvent +- a RouteEvent +- a DispatchEvent + - a DispatchControllerEvent +- a DispatchErrorEvent + - Potentially broken into a RouteUnmatchedEvent, DispatchExceptionEvent, + MiddlewareExceptionEvent, ControllerNotFoundEvent, InvalidControllerEvent, + and InvalidMiddlewareEvent +- a RenderEvent +- a RenderErrorEvent +- a FinishEvent +- a SendResponseEvent (this one is not an MvcEvent, however) + +The event names associated with each would be based on existing event names, +allowing the ability to attach using legacy names OR the class name. + +We can allow using `stopPropagation()`, but have it trigger a deprecation +notice, asking users to use more specific methods of the event to stop +propagation, or, in the case of errors, raising exceptions. + +- `setError()` would cause `isPropagationStopped()` to return true. +- A new method, `setFinalResponse()` would both set the response instance, as + well as cause `isPropagationStopped()` to return true. +- The `RouteEvent` would also halt propagation when `setRouteResult()` is + called. + +Internally, we will also stop using the `*Until()` methods, and instead rely on +the events to handle this for us. If we need a return value, we will instead +pull it from the event on completion. From 8b894241cc37a7449b5554ca4736754eb1ce1a67 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 3 Apr 2019 08:58:33 -0500 Subject: [PATCH 05/26] docs: Mark StoppableEventInterface as deprecated Will remove in version 4.0. --- TODO-PSR-14.md | 4 ++-- src/StoppableEventInterface.php | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 9702893..a6d648d 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -2,13 +2,13 @@ ## 3.3.0 forwards-compatibility release -- [ ] `StoppableEventInterface` implementation +- [x] `StoppableEventInterface` implementation - [x] Create a `StoppableEventInterface` - [x] Make `Event` implement it - [x] Deprecate `propagationIsStopped()` in both `EventInterface` and `Event` - [x] Have `Event::propagationIsStopped()` proxy to `Event::isPropagationStopped()` - [x] Modify `EventManager` internals to use the PSR-14 method if available - - [ ] Mark `StoppableEventInterface` as deprecated + - [x] Mark `StoppableEventInterface` as deprecated - [ ] Listener provider implementation - [ ] Create a `ListenerProvider` subnamespace - [ ] Create a `ListenerProviderInterface` shim diff --git a/src/StoppableEventInterface.php b/src/StoppableEventInterface.php index 3d52a6c..daa9ca2 100644 --- a/src/StoppableEventInterface.php +++ b/src/StoppableEventInterface.php @@ -12,6 +12,9 @@ * * This interface can be mixed into the `Event` instance to make it * forwards-compatible with PSR-14. + * + * @deprecated This interface is a forwards-compatibility shim for use until we can + * provide full PSR-14 compatibility, and will be removed in version 4.0. */ interface StoppableEventInterface { From ed5fb45f97eeeedf9fcc30afd75353b22faba71d Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 3 Apr 2019 09:02:22 -0500 Subject: [PATCH 06/26] feat: ListenerProvider namespace and interface Creates a forwards-compatibility shim for the `ListenerProviderInterface`, in a new subnamespace, `Zend\EventManager\ListenerProviderInterface`. --- TODO-PSR-14.md | 4 ++-- .../ListenerProviderInterface.php | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/ListenerProvider/ListenerProviderInterface.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index a6d648d..d6721f3 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -10,8 +10,8 @@ - [x] Modify `EventManager` internals to use the PSR-14 method if available - [x] Mark `StoppableEventInterface` as deprecated - [ ] Listener provider implementation - - [ ] Create a `ListenerProvider` subnamespace - - [ ] Create a `ListenerProviderInterface` shim + - [x] Create a `ListenerProvider` subnamespace + - [x] Create a `ListenerProviderInterface` shim - [ ] Create a `PrioritizedListenerProvider` interface extending the `ListenerProviderInterface` and defining a `getListenersForEventByPriority($event, array $identifiers = []) : array` method. diff --git a/src/ListenerProvider/ListenerProviderInterface.php b/src/ListenerProvider/ListenerProviderInterface.php new file mode 100644 index 0000000..20b4e70 --- /dev/null +++ b/src/ListenerProvider/ListenerProviderInterface.php @@ -0,0 +1,21 @@ + Date: Wed, 3 Apr 2019 09:06:31 -0500 Subject: [PATCH 07/26] feat: Creates PrioritizedListenerProviderInterface For use in getting a lookup table of priorities and associated listeners, optionally using identifiers for lookup. --- TODO-PSR-14.md | 2 +- .../PrioritizedListenerProviderInterface.php | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/ListenerProvider/PrioritizedListenerProviderInterface.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index d6721f3..053b776 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -12,7 +12,7 @@ - [ ] Listener provider implementation - [x] Create a `ListenerProvider` subnamespace - [x] Create a `ListenerProviderInterface` shim - - [ ] Create a `PrioritizedListenerProvider` interface extending the + - [x] Create a `PrioritizedListenerProvider` interface extending the `ListenerProviderInterface` and defining a `getListenersForEventByPriority($event, array $identifiers = []) : array` method. - [ ] Create a `PrioritizedListenerAttachmentInterface`, defining: diff --git a/src/ListenerProvider/PrioritizedListenerProviderInterface.php b/src/ListenerProvider/PrioritizedListenerProviderInterface.php new file mode 100644 index 0000000..82ad87e --- /dev/null +++ b/src/ListenerProvider/PrioritizedListenerProviderInterface.php @@ -0,0 +1,20 @@ + Returns a hash table of priorities with + * the associated listeners for that priority. + */ + public function getListenersForEventByPriority($event, array $identifiers = []); +} From 24461457b1267ea6e4fc6a896e92676a787eb4d1 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 3 Apr 2019 09:19:37 -0500 Subject: [PATCH 08/26] feat: Creates PrioritizedListenerAttachmentInterface This provides the methods necessary for attaching listeners. It does not extend PrioritizedListenerProviderInterface, as we want to be able to re-use that particular interface with shared providers, which will have a different attachment mechanism in version 3 releases. --- TODO-PSR-14.md | 11 +-- ...PrioritizedListenerAttachmentInterface.php | 69 +++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 src/ListenerProvider/PrioritizedListenerAttachmentInterface.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 053b776..1db660a 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -15,13 +15,16 @@ - [x] Create a `PrioritizedListenerProvider` interface extending the `ListenerProviderInterface` and defining a `getListenersForEventByPriority($event, array $identifiers = []) : array` method. - - [ ] Create a `PrioritizedListenerAttachmentInterface`, defining: - - [ ] `attach($event, callable $listener, $priority = 1)` (where `$event` + - [x] Create a `PrioritizedListenerAttachmentInterface`, defining: + - [x] `attach($event, callable $listener, $priority = 1)` (where `$event` can be an object or string name) - - [ ] `detach(callable $listener, $event = null, $force = false)` (where `$event` + - [x] `detach(callable $listener, $event = null, $force = false)` (where `$event` can be an object or string name and `$force` is boolean) - - [ ] `attachWildcardListener(callable $listener, $priority = 1)` + - [x] `attachWildcardListener(callable $listener, $priority = 1)` (`attach('*', $listener, $priority)` will proxy to this method) + - [x] `detachWildcardListener(callable $listener, $force = false)` + (`detach($listener, '*', $force)` will proxy to this method) + - [x] `clearListeners($event)` - [ ] Create a `PrioritizedListenerProvider` implementation of the above based on the internals of `EventManager` - [ ] attachment/detachment diff --git a/src/ListenerProvider/PrioritizedListenerAttachmentInterface.php b/src/ListenerProvider/PrioritizedListenerAttachmentInterface.php new file mode 100644 index 0000000..40cfbd5 --- /dev/null +++ b/src/ListenerProvider/PrioritizedListenerAttachmentInterface.php @@ -0,0 +1,69 @@ + + * attach('*', $listener, $priority) + * + * + * The above will actually invoke this method instead. + * + * @param callable $listener The listener to attach. + * @param int $priority The priority at which to attach the listener. + * High priorities respond earlier; negative priorities respond later. + * @return void + */ + public function attachWildcardListener(callable $listener, $priority = 1); + + /** + * Detaches a wildcard listener. + * + * Analagous to: + * + * + * detach($listener, '*', $force) + * + * + * The above will actually invoke this method instead. + * + * @param callable $listener The listener to detach. + * @return void + */ + public function detachWildcardListener(callable $listener); + + /** + * @param string $event The event for which to remove listeners. + * @return void + */ + public function clearListeners($event); +} From 650a6f660be6eeebfbcae262504ba1b2b617c9bd Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 3 Apr 2019 12:14:05 -0500 Subject: [PATCH 09/26] feat: Creates PrioritizedListenerProvider New provider implements `PrioritizedListenerAttachmentInterface` and `PrioritizedListenerProviderInterface`, and will iterate attached listeners in priority order. Each iteration will take into account both the event name, if a `getName()` method is available, the event class, and any wildcard listeners, and listeners of the same priority will be returned in the order they are attached, based on those criteria. --- TODO-PSR-14.md | 12 +- .../PrioritizedListenerProvider.php | 207 +++++++++++++++ .../PrioritizedListenerProviderTest.php | 241 ++++++++++++++++++ 3 files changed, 454 insertions(+), 6 deletions(-) create mode 100644 src/ListenerProvider/PrioritizedListenerProvider.php create mode 100644 test/ListenerProvider/PrioritizedListenerProviderTest.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 1db660a..33ede96 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -25,15 +25,15 @@ - [x] `detachWildcardListener(callable $listener, $force = false)` (`detach($listener, '*', $force)` will proxy to this method) - [x] `clearListeners($event)` - - [ ] Create a `PrioritizedListenerProvider` implementation of the above based + - [x] Create a `PrioritizedListenerProvider` implementation of the above based on the internals of `EventManager` - - [ ] attachment/detachment - - [ ] getListenersForEvent should take into account event name if an EventInterface - - [ ] getListenersForEvent should also pull wildcard listeners - - [ ] getListenersForEvent should accept an optional second argument, an + - [x] attachment/detachment + - [x] getListenersForEvent should take into account event name if an EventInterface + - [x] getListenersForEvent should also pull wildcard listeners + - [x] getListenersForEvent should accept an optional second argument, an array of identifiers. This method will return all listeners in prioritized order. - - [ ] implement `getListenersForEventByPriority` + - [x] implement `getListenersForEventByPriority` - [ ] Create a `PrioritizedIdentifierListenerProvider` that implements both the `PrioritizedListenerProvider` interface and the `SharedEventManagerInterface` diff --git a/src/ListenerProvider/PrioritizedListenerProvider.php b/src/ListenerProvider/PrioritizedListenerProvider.php new file mode 100644 index 0000000..4b46d23 --- /dev/null +++ b/src/ListenerProvider/PrioritizedListenerProvider.php @@ -0,0 +1,207 @@ + => [ + * => [ + * 0 => [, ...] + * ], + * ... + * ], + * ... + * ] + * + * NOTE: + * This structure helps us to reuse the list of listeners + * instead of first iterating over it and generating a new one + * -> In result it improves performance by up to 25% even if it looks a bit strange + * + * @var array> + */ + protected $events = []; + + /** + * {@inheritDoc} + */ + public function getListenersForEvent($event) + { + yield from $this->iterateByPriority( + $this->getListenersForEventByPriority($event) + ); + } + + /** + * {@inheritDoc} + * @param string[] $identifiers Ignored in this implementation. + * @throws Exception\InvalidArgumentException for invalid $event types. + */ + public function getListenersForEventByPriority($event, array $identifiers = []) + { + if (! is_object($event)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects the $event argument to be an object; received %s', + __METHOD__, + gettype($event) + )); + } + + $identifiers = is_callable([$event, 'getName']) + ? [$event->getName()] + : []; + $identifiers = array_merge($identifiers, [get_class($event), '*']); + + $prioritizedListeners = []; + foreach ($identifiers as $name) { + if (! isset($this->events[$name])) { + continue; + } + + foreach ($this->events[$name] as $priority => $listOfListeners) { + $prioritizedListeners[$priority][] = $listOfListeners[0]; + } + } + + return $prioritizedListeners; + } + + /** + * {@inheritDoc} + * @throws Exception\InvalidArgumentException for invalid $event types. + */ + public function attach($event, callable $listener, $priority = 1) + { + if (! is_string($event)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a string for the event; received %s', + __METHOD__, + gettype($event) + )); + } + + $this->events[$event][(int) $priority][0][] = $listener; + } + + /** + * {@inheritDoc} + * @param bool $force Internal; used by attachWildcardListener to force + * removal of the '*' event. + * @throws Exception\InvalidArgumentException for invalid event types. + */ + public function detach(callable $listener, $event = null, $force = false) + { + if (null === $event || ('*' === $event && ! $force)) { + $this->detachWildcardListener($listener); + return; + } + + if (! is_string($event)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a string for the event; received %s', + __METHOD__, + gettype($event) + )); + } + + if (! isset($this->events[$event])) { + return; + } + + foreach ($this->events[$event] as $priority => $listeners) { + foreach ($listeners[0] as $index => $evaluatedListener) { + if ($evaluatedListener !== $listener) { + continue; + } + + // Found the listener; remove it. + unset($this->events[$event][$priority][0][$index]); + + // If the queue for the given priority is empty, remove it. + if (empty($this->events[$event][$priority][0])) { + unset($this->events[$event][$priority]); + break; + } + } + } + + // If the queue for the given event is empty, remove it. + if (empty($this->events[$event])) { + unset($this->events[$event]); + } + } + + /** + * {@inheritDoc} + */ + public function attachWildcardListener(callable $listener, $priority = 1) + { + $this->events['*'][(int) $priority][0][] = $listener; + } + + /** + * {@inheritDoc} + */ + public function detachWildcardListener(callable $listener) + { + foreach (array_keys($this->events) as $event) { + $this->detach($listener, $event, true); + } + } + + /** + * {@inheritDoc} + * @throws Exception\InvalidArgumentException for invalid event types. + */ + public function clearListeners($event) + { + if (! is_string($event)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a string for the event; received %s', + __METHOD__, + gettype($event) + )); + } + + if (isset($this->events[$event])) { + unset($this->events[$event]); + } + } + + /** + * @param array $prioritizedListeners + * @return iterable + */ + private function iterateByPriority($prioritizedListeners) + { + krsort($prioritizedListeners); + foreach ($prioritizedListeners as $listenerSets) { + yield from $this->iterateListenerSets($listenerSets); + } + } + + /** + * @param iterable $listenerSets + * @return iterable + */ + private function iterateListenerSets($listenerSets) + { + foreach ($listenerSets as $listOfListeners) { + yield from $listOfListeners; + } + } +} diff --git a/test/ListenerProvider/PrioritizedListenerProviderTest.php b/test/ListenerProvider/PrioritizedListenerProviderTest.php new file mode 100644 index 0000000..f42961f --- /dev/null +++ b/test/ListenerProvider/PrioritizedListenerProviderTest.php @@ -0,0 +1,241 @@ +provider = new PrioritizedListenerProvider(); + } + + public function createEvent() + { + $accumulator = new SplQueue(); + $event = new Event(); + $event->setName('test'); + $event->setTarget($this); + $event->setParams(compact('accumulator')); + return $event; + } + + public function createListener($return) + { + return function ($event) use ($return) { + $event->getParam('accumulator')->enqueue($return); + }; + } + + /** + * @param object $event + */ + public function triggerListeners(PrioritizedListenerProvider $provider, $event) + { + foreach ($provider->getListenersForEvent($event) as $listener) { + $listener($event); + } + } + + /** + * @param iterable $listeners + * @return array + */ + public function flattenListeners($listeners) + { + $flattened = []; + foreach ($listeners as $listener) { + $flattened[] = $listener; + } + return $flattened; + } + + public function testIteratesListenersOfDifferentPrioritiesInPriorityOrder() + { + for ($i = -1; $i < 5; $i += 1) { + $this->provider->attach('test', $this->createListener($i), $i); + } + + $event = $this->createEvent(); + $this->triggerListeners($this->provider, $event); + + $values = iterator_to_array($event->getParam('accumulator')); + $this->assertEquals( + [4, 3, 2, 1, 0, -1], + $values, + sprintf("Did not receive values in priority order: %s\n", var_export($values, 1)) + ); + } + + public function testIteratesListenersOfSamePriorityInAttachmentOrder() + { + for ($i = -1; $i < 5; $i += 1) { + $this->provider->attach('test', $this->createListener($i)); + } + + $event = $this->createEvent(); + $this->triggerListeners($this->provider, $event); + + $values = iterator_to_array($event->getParam('accumulator')); + $this->assertEquals( + [-1, 0, 1, 2, 3, 4], + $values, + sprintf("Did not receive values in attachment order: %s\n", var_export($values, 1)) + ); + } + + public function testIteratesWildcardListenersAfterExplicitListenersOfSamePriority() + { + $this->provider->attachWildcardListener($this->createListener(2), 5); + $this->provider->attach('test', $this->createListener(1), 5); + $this->provider->attachWildcardListener($this->createListener(3), 5); + + $event = $this->createEvent(); + $this->triggerListeners($this->provider, $event); + + $values = iterator_to_array($event->getParam('accumulator')); + $this->assertEquals( + [1, 2, 3], + $values, + sprintf("Did not receive wildcard values after explicit listeners: %s\n", var_export($values, 1)) + ); + } + + public function testIteratesListenersAttachedToClassNameAfterThoseByNameWhenOfSamePriority() + { + $this->provider->attach(Event::class, $this->createListener(2), 5); + $this->provider->attach('test', $this->createListener(1), 5); + $this->provider->attach(Event::class, $this->createListener(3), 5); + + $event = $this->createEvent(); + $this->triggerListeners($this->provider, $event); + + $values = iterator_to_array($event->getParam('accumulator')); + $this->assertEquals( + [1, 2, 3], + $values, + sprintf("Did not receive class-name values after event-name values: %s\n", var_export($values, 1)) + ); + } + + public function testIteratesListenersAttachedToClassNameBeforeWildcardsWhenOfSamePriority() + { + $this->provider->attachWildcardListener($this->createListener(2), 5); + $this->provider->attach(Event::class, $this->createListener(1), 5); + $this->provider->attachWildcardListener($this->createListener(3), 5); + + $event = $this->createEvent(); + $this->triggerListeners($this->provider, $event); + + $values = iterator_to_array($event->getParam('accumulator')); + $this->assertEquals( + [1, 2, 3], + $values, + sprintf("Did not receive class-name values before wildcard values: %s\n", var_export($values, 1)) + ); + } + + public function testCanAttachAndIterateUsingOnlyEventClass() + { + $expected = ['value']; + $this->provider->attach(SplQueue::class, function (SplQueue $event) { + $event->enqueue('value'); + }); + + $event = new SplQueue(); + $this->triggerListeners($this->provider, $event); + + $values = iterator_to_array($event); + $this->assertSame($values, $expected); + } + + public function testCanDetachPreviouslyAttachedListenerFromEvent() + { + $listener = function ($event) { + }; + $this->provider->attach('test', $listener); + + $event = $this->createEvent(); + $listeners = iterator_to_array($this->provider->getListenersForEvent($event)); + $this->assertSame([$listener], $listeners, 'Expected one listener for event; none found?'); + + $this->provider->detach($listener, 'test'); + $listeners = iterator_to_array($this->provider->getListenersForEvent($event)); + $this->assertSame([], $listeners, 'Listener found after detachment, and should not be'); + } + + public function testCanDetachListenerFromAllEventsUsingNullEventToDetach() + { + $listener = function ($event) { + }; + $this->provider->attach('test', $listener); + $this->provider->attach(Event::class, $listener); + + $event = $this->createEvent(); + $listeners = $this->flattenListeners($this->provider->getListenersForEvent($event)); + $this->assertSame([$listener, $listener], $listeners); + + $this->provider->detach($listener); + $listeners = iterator_to_array($this->provider->getListenersForEvent($event)); + $this->assertSame([], $listeners, 'Listener found after detachment, and should not be'); + } + + public function testCanDetachListenerFromAllEventsViaDetachWildcardListener() + { + $listener = function ($event) { + }; + $this->provider->attach('test', $listener); + $this->provider->attach(Event::class, $listener); + + $event = $this->createEvent(); + $listeners = $this->flattenListeners($this->provider->getListenersForEvent($event)); + $this->assertSame([$listener, $listener], $listeners); + + $this->provider->detachWildcardListener($listener); + $listeners = iterator_to_array($this->provider->getListenersForEvent($event)); + $this->assertSame([], $listeners, 'Listeners found after detachment, and should not be'); + } + + public function testCanDetachWildcardListenerFromAllEvents() + { + $listener = function ($event) { + }; + $this->provider->attachWildcardListener($listener); + $this->provider->attach('test', $listener); + $this->provider->attach(Event::class, $listener); + + $event = $this->createEvent(); + $listeners = $this->flattenListeners($this->provider->getListenersForEvent($event)); + $this->assertSame([$listener, $listener, $listener], $listeners); + + $this->provider->detachWildcardListener($listener); + $listeners = iterator_to_array($this->provider->getListenersForEvent($event)); + $this->assertSame([], $listeners, 'Listeners found after detachment, and should not be'); + } + + public function testCanClearListenersForASingleEventName() + { + $listener = function ($event) { + }; + $this->provider->attachWildcardListener($listener); + $this->provider->attach('test', $listener); + $this->provider->attach(Event::class, $listener); + + $event = $this->createEvent(); + $listeners = $this->flattenListeners($this->provider->getListenersForEvent($event)); + $this->assertSame([$listener, $listener, $listener], $listeners); + + $this->provider->clearListeners('test'); + $listeners = $this->flattenListeners($this->provider->getListenersForEvent($event)); + $this->assertSame([$listener, $listener], $listeners); + } +} From c8b7d6ac7a2d49680efec0c1745a41170481f252 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 3 Apr 2019 16:17:48 -0500 Subject: [PATCH 10/26] feat: Creates PrioritizedIdentifierListenerProvider The PrioritizedIdentifierListenerProvider mimics functionality present in the SharedEventManager (and implements the SharedEventManagerInterface). Its purpose is to be a drop-in replacement for the `SharedEventManager` to allow users to start migrating to PSR-14 functionality. In the process of working on this implementation, I discovered some complexity in the data structure returned from `getListenersForEventByPriority` implementation of `PrioritizedListenerProvider` that, when mimiced in `PrioritizedIdentifierListenerProvider`, made verifying behavior difficult. In particular, it was this line: ```php $prioritizedListeners[$priority][] = $listOfListeners[0]; ``` The problem that arose is that the `$prioritizedListeners` returned were now two levels deep, which made comparisons far harder. I changed this to read: ``` $prioritizedListeners[$priority] = isset($prioritizedListeners[$priority]) ? array_merge($prioritizedListeners[$priority], $listOfListeners[0]) : $listOfListeners[0]; ``` This makes the return value far simpler, and _should_ keep speed reasonable, though I have yet to benchmark it. --- TODO-PSR-14.md | 8 +- .../PrioritizedIdentifierListenerProvider.php | 278 ++++++++++++++++ .../PrioritizedListenerProvider.php | 19 +- ...oritizedIdentifierListenerProviderTest.php | 311 ++++++++++++++++++ 4 files changed, 598 insertions(+), 18 deletions(-) create mode 100644 src/ListenerProvider/PrioritizedIdentifierListenerProvider.php create mode 100644 test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 33ede96..96ab8ad 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -34,12 +34,12 @@ array of identifiers. This method will return all listeners in prioritized order. - [x] implement `getListenersForEventByPriority` - - [ ] Create a `PrioritizedIdentifierListenerProvider` that implements + - [x] Create a `PrioritizedIdentifierListenerProvider` that implements both the `PrioritizedListenerProvider` interface and the `SharedEventManagerInterface` - - [ ] implement `getListenersForEventByPriority` - - [ ] `SharedEventManager` will extend this class - - [ ] mark as deprecated (will not use this in v4) + - [x] implement `getListenersForEventByPriority` + - [x] `SharedEventManager` will extend this class + - [x] mark as deprecated (will not use this in v4) - [ ] Create a `PrioritizedAggregateListenerProvider` implementation - [ ] Accepts a list of `PrioritizedListenerProvider` instances - [ ] `getListenersByEvent()` will loop through each, in order, calling the diff --git a/src/ListenerProvider/PrioritizedIdentifierListenerProvider.php b/src/ListenerProvider/PrioritizedIdentifierListenerProvider.php new file mode 100644 index 0000000..a37cb3b --- /dev/null +++ b/src/ListenerProvider/PrioritizedIdentifierListenerProvider.php @@ -0,0 +1,278 @@ +>> + */ + protected $identifiers = []; + + /** + * {@inheritDoc} + * @param array $identifiers Identifiers from which to match event listeners. + * @throws Exception\InvalidArgumentException for invalid event types + * @throws Exception\InvalidArgumentException for invalid identifier types + */ + public function getListenersForEvent($event, array $identifiers = []) + { + yield from $this->iterateByPriority( + $this->getListenersForEventByPriority($event, $identifiers) + ); + } + + /** + * {@inheritDoc} + * @throws Exception\InvalidArgumentException for invalid event types + * @throws Exception\InvalidArgumentException for invalid identifier types + */ + public function getListenersForEventByPriority($event, array $identifiers = []) + { + $this->validateEventForListenerRetrieval($event, __METHOD__); + + $prioritizedListeners = []; + $identifiers = $this->normalizeIdentifierList($identifiers); + $eventList = $this->getEventList($event); + + foreach ($identifiers as $identifier) { + if (! is_string($identifier) || empty($identifier)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Identifier names passed to %s must be non-empty', + __METHOD__ + )); + } + + if (! isset($this->identifiers[$identifier])) { + continue; + } + + $listenersByIdentifier = $this->identifiers[$identifier]; + + foreach ($eventList as $eventName) { + if (! isset($listenersByIdentifier[$eventName])) { + continue; + } + + foreach ($listenersByIdentifier[$eventName] as $priority => $listOfListeners) { + $prioritizedListeners[$priority] = isset($prioritizedListeners[$priority]) + ? array_merge($prioritizedListeners[$priority], $listOfListeners[0]) + : $listOfListeners[0]; + } + } + } + + return $prioritizedListeners; + } + + /** + * {@inheritDoc} + * @throws Exception\InvalidArgumentException for invalid identifier types + * @throws Exception\InvalidArgumentException for invalid event types + */ + public function attach($identifier, $eventName, callable $listener, $priority = 1) + { + if (! is_string($identifier) || empty($identifier)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid identifier provided; must be a string; received "%s"', + gettype($identifier) + )); + } + + if (! is_string($eventName) || empty($eventName)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid event provided; must be a non-empty string; received "%s"', + gettype($eventName) + )); + } + + $this->identifiers[$identifier][$eventName][(int) $priority][0][] = $listener; + } + + /** + * {@inheritDoc} + * @param bool $force Internal; allows recursing when detaching wildcard listeners + * @throws Exception\InvalidArgumentException for invalid identifier types + * @throws Exception\InvalidArgumentException for invalid event name types + */ + public function detach(callable $listener, $identifier = null, $eventName = null, $force = false) + { + // No identifier or wildcard identifier: loop through all identifiers and detach + if (null === $identifier || ('*' === $identifier && ! $force)) { + foreach (array_keys($this->identifiers) as $identifier) { + $this->detach($listener, $identifier, $eventName, true); + } + return; + } + + if (! is_string($identifier) || empty($identifier)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid identifier provided; must be a string, received %s', + gettype($identifier) + )); + } + + // Do we have any listeners on the provided identifier? + if (! isset($this->identifiers[$identifier])) { + return; + } + + if (null === $eventName || ('*' === $eventName && ! $force)) { + foreach (array_keys($this->identifiers[$identifier]) as $eventName) { + $this->detach($listener, $identifier, $eventName, true); + } + return; + } + + if (! is_string($eventName) || empty($eventName)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid event name provided; must be a string, received %s', + gettype($eventName) + )); + } + + if (! isset($this->identifiers[$identifier][$eventName])) { + return; + } + + foreach ($this->identifiers[$identifier][$eventName] as $priority => $listOfListeners) { + foreach ($listOfListeners[0] as $index => $evaluatedListener) { + if ($evaluatedListener !== $listener) { + continue; + } + + // Found the listener; remove it. + unset($this->identifiers[$identifier][$eventName][$priority][0][$index]); + + // Is the priority queue empty? + if (empty($this->identifiers[$identifier][$eventName][$priority][0])) { + unset($this->identifiers[$identifier][$eventName][$priority]); + break; + } + } + + // Is the event queue empty? + if (empty($this->identifiers[$identifier][$eventName])) { + unset($this->identifiers[$identifier][$eventName]); + break; + } + } + + // Is the identifier queue now empty? Remove it. + if (empty($this->identifiers[$identifier])) { + unset($this->identifiers[$identifier]); + } + } + + /** + * {@inheritDoc} + */ + public function getListeners(array $identifiers, $eventName) + { + return $this->getListenersForEventByPriority($eventName, $identifiers); + } + + /** + * {@inheritDoc} + */ + public function clearListeners($identifier, $eventName = null) + { + if (! isset($this->identifiers[$identifier])) { + return false; + } + + if (null === $eventName) { + unset($this->identifiers[$identifier]); + return; + } + + if (! isset($this->identifiers[$identifier][$eventName])) { + return; + } + + unset($this->identifiers[$identifier][$eventName]); + } + + /** + * @param mixed $event Event to validate + * @param string $method Method name invoking this one + * @return void + * @throws Exception\InvalidArgumentException for invalid event types + */ + private function validateEventForListenerRetrieval($event, $method) + { + if (is_object($event)) { + return; + } + + if (is_string($event) && '*' !== $event && ! empty($event)) { + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Event name passed to %s must be a non-empty, non-wildcard string or an object', + $method + )); + } + + /** + * Deduplicate identifiers, and ensure wildcard identifier is last. + * + * @return string[] + */ + private function normalizeIdentifierList(array $identifiers) + { + $identifiers = array_unique($identifiers); + if (false !== ($index = array_search('*', $identifiers, true))) { + unset($identifiers[$index]); + } + array_push($identifiers, '*'); + return $identifiers; + } + + /** + * @param string|object $event + * @return string[] + */ + private function getEventList($event) + { + if (is_string($event)) { + return [$event, '*']; + } + + return is_callable([$event, 'getName']) + ? [$event->getName(), get_class($event), '*'] + : [get_class($event), '*']; + } + + /** + * @param array $prioritizedListeners + * @return iterable + */ + private function iterateByPriority($prioritizedListeners) + { + krsort($prioritizedListeners); + foreach ($prioritizedListeners as $listeners) { + yield from $listeners; + } + } +} diff --git a/src/ListenerProvider/PrioritizedListenerProvider.php b/src/ListenerProvider/PrioritizedListenerProvider.php index 4b46d23..8f8acee 100644 --- a/src/ListenerProvider/PrioritizedListenerProvider.php +++ b/src/ListenerProvider/PrioritizedListenerProvider.php @@ -73,7 +73,9 @@ public function getListenersForEventByPriority($event, array $identifiers = []) } foreach ($this->events[$name] as $priority => $listOfListeners) { - $prioritizedListeners[$priority][] = $listOfListeners[0]; + $prioritizedListeners[$priority] = isset($prioritizedListeners[$priority]) + ? array_merge($prioritizedListeners[$priority], $listOfListeners[0]) + : $listOfListeners[0]; } } @@ -189,19 +191,8 @@ public function clearListeners($event) private function iterateByPriority($prioritizedListeners) { krsort($prioritizedListeners); - foreach ($prioritizedListeners as $listenerSets) { - yield from $this->iterateListenerSets($listenerSets); - } - } - - /** - * @param iterable $listenerSets - * @return iterable - */ - private function iterateListenerSets($listenerSets) - { - foreach ($listenerSets as $listOfListeners) { - yield from $listOfListeners; + foreach ($prioritizedListeners as $listeners) { + yield from $listeners; } } } diff --git a/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php b/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php new file mode 100644 index 0000000..6dc605a --- /dev/null +++ b/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php @@ -0,0 +1,311 @@ +callback = function ($e) { + }; + $this->provider = new PrioritizedIdentifierListenerProvider(); + } + + public function getListeners(PrioritizedIdentifierListenerProvider $provider, array $identifiers, $event, $priority = 1) + { + $priority = (int) $priority; + $listeners = $provider->getListenersForEventByPriority($event, $identifiers); + if (! isset($listeners[$priority])) { + return []; + } + return $listeners[$priority]; + } + + public function invalidIdentifiers() + { + return [ + 'null' => [null], + 'true' => [true], + 'false' => [false], + 'zero' => [0], + 'int' => [1], + 'zero-float' => [0.0], + 'float' => [1.1], + 'empty-string' => [''], + 'array' => [['test', 'foo']], + 'non-traversable-object' => [(object) ['foo' => 'bar']], + ]; + } + + /** + * @dataProvider invalidIdentifiers + */ + public function testAttachRaisesExceptionForInvalidIdentifer($identifier) + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('identifier'); + $this->provider->attach($identifier, 'foo', $this->callback); + } + + public function invalidEventNames() + { + return [ + 'null' => [null], + 'true' => [true], + 'false' => [false], + 'zero' => [0], + 'int' => [1], + 'zero-float' => [0.0], + 'float' => [1.1], + 'empty-string' => [''], + 'array' => [['foo', 'bar']], + 'non-traversable-object' => [(object) ['foo' => 'bar']], + ]; + } + + /** + * @dataProvider invalidEventNames + */ + public function testAttachRaisesExceptionForInvalidEvent($event) + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('event'); + $this->provider->attach('foo', $event, $this->callback); + } + + public function testCanAttachListeners() + { + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + + $listeners = $this->getListeners($this->provider, ['IDENTIFIER'], 'EVENT'); + $this->assertSame([$this->callback], $listeners); + } + + public function detachIdentifierAndEvent() + { + return [ + 'null-identifier-and-null-event' => [null, null], + 'same-identifier-and-null-event' => ['IDENTIFIER', null], + 'null-identifier-and-same-event' => [null, 'EVENT'], + 'same-identifier-and-same-event' => ['IDENTIFIER', 'EVENT'], + ]; + } + + /** + * @dataProvider detachIdentifierAndEvent + */ + public function testCanDetachListenersUsingIdentifierAndEvent($identifier, $event) + { + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + $this->provider->detach($this->callback, $identifier, $event); + $listeners = $this->getListeners($this->provider, ['IDENTIFIER'], 'EVENT'); + $this->assertSame([], $listeners); + } + + public function testDetachDoesNothingIfIdentifierNotInProvider() + { + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + $this->provider->detach($this->callback, 'DIFFERENT-IDENTIFIER'); + + $listeners = $this->getListeners($this->provider, ['IDENTIFIER'], 'EVENT'); + $this->assertSame([$this->callback], $listeners); + } + + public function testDetachDoesNothingIfIdentifierDoesNotContainEvent() + { + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + $this->provider->detach($this->callback, 'IDENTIFIER', 'DIFFERENT-EVENT'); + $listeners = $this->getListeners($this->provider, ['IDENTIFIER'], 'EVENT'); + $this->assertSame([$this->callback], $listeners); + } + + public function testProviderReturnsEmptyListWhenNoListenersAttachedForEventAndIdentifier() + { + $test = $this->provider->getListenersForEvent('EVENT', ['IDENTIFIER']); + $this->assertInternalType('iterable', $test); + $this->assertCount(0, $test); + } + + public function testProviderReturnsAllListenersIncludingWildcardListenersForEvent() + { + $callback1 = clone $this->callback; + $callback2 = clone $this->callback; + $callback3 = clone $this->callback; + $callback4 = clone $this->callback; + + $this->provider->attach('IDENTIFIER', 'EVENT', $callback1); + $this->provider->attach('IDENTIFIER', '*', $callback2); + $this->provider->attach('*', 'EVENT', $callback3); + $this->provider->attach('IDENTIFIER', 'EVENT', $callback4); + + $test = $this->getListeners($this->provider, [ 'IDENTIFIER' ], 'EVENT'); + $this->assertEquals([ + $callback1, + $callback4, + $callback2, + $callback3, + ], $test); + } + + public function testClearListenersWhenNoEventIsProvidedRemovesAllListenersForTheIdentifier() + { + $wildcardIdentifier = clone $this->callback; + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + $this->provider->attach('IDENTIFIER', '*', $this->callback); + $this->provider->attach('*', 'EVENT', $wildcardIdentifier); + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + + $this->provider->clearListeners('IDENTIFIER'); + + $listeners = $this->getListeners($this->provider, [ 'IDENTIFIER' ], 'EVENT'); + $this->assertSame( + [$wildcardIdentifier], + $listeners, + sprintf( + 'Listener list should contain only wildcard identifier listener; received: %s', + var_export($listeners, 1) + ) + ); + } + + public function testClearListenersRemovesAllExplicitListenersForGivenIdentifierAndEvent() + { + $alternate = clone $this->callback; + $wildcard = clone $this->callback; + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + $this->provider->attach('IDENTIFIER', 'ALTERNATE', $alternate); + $this->provider->attach('*', 'EVENT', $wildcard); + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + + $this->provider->clearListeners('IDENTIFIER', 'EVENT'); + + $listeners = $this->getListeners($this->provider, ['IDENTIFIER'], 'EVENT'); + $this->assertInternalType('array', $listeners, 'Unexpected return value from getListeners() for event EVENT'); + $this->assertCount(1, $listeners); + $listener = array_shift($listeners); + $this->assertSame($wildcard, $listener, sprintf( + 'Expected only wildcard listener on event EVENT after clearListener operation; received: %s', + var_export($listener, 1) + )); + + $listeners = $this->getListeners($this->provider, ['IDENTIFIER'], 'ALTERNATE'); + $this->assertInternalType( + 'array', + $listeners, + 'Unexpected return value from getListeners() for event ALTERNATE' + ); + $this->assertCount(1, $listeners); + $listener = array_shift($listeners); + $this->assertSame($alternate, $listener, 'Unexpected listener list for event ALTERNATE'); + } + + public function testClearListenersDoesNotRemoveWildcardListenersWhenEventIsProvided() + { + $wildcardEventListener = clone $this->callback; + $wildcardIdentifierListener = clone $this->callback; + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + $this->provider->attach('IDENTIFIER', '*', $wildcardEventListener); + $this->provider->attach('*', 'EVENT', $wildcardIdentifierListener); + $this->provider->attach('IDENTIFIER', 'EVENT', $this->callback); + + // REMOVE + $this->provider->getListenersForEventByPriority('EVENT', ['IDENTIFIER']); + + $this->provider->clearListeners('IDENTIFIER', 'EVENT'); + + $listeners = $this->getListeners($this->provider, ['IDENTIFIER'], 'EVENT'); + $this->assertContains( + $wildcardEventListener, + $listeners, + 'Event listener list after clear operation does not include wildcard event listener' + ); + $this->assertContains( + $wildcardIdentifierListener, + $listeners, + 'Event listener list after clear operation does not include wildcard identifier listener' + ); + $this->assertNotContains( + $this->callback, + $listeners, + 'Event listener list after clear operation includes explicitly attached listener and should not' + ); + } + + public function testClearListenersDoesNothingIfNoEventsRegisteredForIdentifier() + { + $callback = clone $this->callback; + $this->provider->attach('IDENTIFIER', 'NOTEVENT', $this->callback); + $this->provider->attach('*', 'EVENT', $this->callback); + + $this->provider->clearListeners('IDENTIFIER', 'EVENT'); + + // getListeners() always pulls in wildcard listeners + $this->assertEquals([$this->callback], $this->getListeners($this->provider, [ 'IDENTIFIER' ], 'EVENT')); + } + + public function invalidIdentifiersAndEvents() + { + $types = $this->invalidIdentifiers(); + unset($types['null']); + return $types; + } + + /** + * @dataProvider invalidIdentifiersAndEvents + */ + public function testDetachingWithInvalidIdentifierTypeRaisesException($identifier) + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid identifier'); + $this->provider->detach($this->callback, $identifier, 'test'); + } + + /** + * @dataProvider invalidIdentifiersAndEvents + */ + public function testDetachingWithInvalidEventTypeRaisesException($eventName) + { + $this->provider->attach('IDENTIFIER', '*', $this->callback); + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid event name'); + $this->provider->detach($this->callback, 'IDENTIFIER', $eventName); + } + + public function invalidEventNamesForFetchingListeners() + { + $types = $this->invalidEventNames(); + unset($types['non-traversable-object']); + yield from $types; + } + + /** + * @dataProvider invalidEventNamesForFetchingListeners + */ + public function testRetrievingListenersRaisesExceptionForInvalidEventName($eventName) + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('must be a non-empty'); + $this->provider->getListenersForEventByPriority($eventName, ['IDENTIFIER']); + } + + /** + * @dataProvider invalidIdentifiers + */ + public function testRetrievingListenersRaisesExceptionForInvalidIdentifier($identifier) + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('must be non-empty'); + $this->provider->getListenersForEventByPriority('EVENT', [$identifier]); + } +} From 5569b4e8157bda85bc40878ea22c800fa625fdcf Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 4 Apr 2019 10:30:02 -0500 Subject: [PATCH 11/26] feat: Creates a PrioritizedAggregateListenerProvider This version acts like the combination of EventManager+SharedEventManager in terms of how it aggregates and resolves priority for listeners. The class aggregates a list of `PrioritizedListenerAttachmentInterface` instances (and implements the interface itself), looping over each in ordert to build up a prioritized list of all listeners from all providers. Since they are done in order, the order in which they should be attached generally is: - PrioritizedListenerProvider - PrioritizedIdentifierListenerProvider --- TODO-PSR-14.md | 6 +- .../PrioritizedAggregateListenerProvider.php | 82 +++++++++++++ ...ioritizedAggregateListenerProviderTest.php | 108 ++++++++++++++++++ 3 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 src/ListenerProvider/PrioritizedAggregateListenerProvider.php create mode 100644 test/ListenerProvider/PrioritizedAggregateListenerProviderTest.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 96ab8ad..9eb4d09 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -40,9 +40,9 @@ - [x] implement `getListenersForEventByPriority` - [x] `SharedEventManager` will extend this class - [x] mark as deprecated (will not use this in v4) - - [ ] Create a `PrioritizedAggregateListenerProvider` implementation - - [ ] Accepts a list of `PrioritizedListenerProvider` instances - - [ ] `getListenersByEvent()` will loop through each, in order, calling the + - [x] Create a `PrioritizedAggregateListenerProvider` implementation + - [x] Accepts a list of `PrioritizedListenerProvider` instances + - [x] `getListenersByEvent()` will loop through each, in order, calling the `getListenersForEventByPriority()` method of each, returning the aggregated listeners in priority order. - [ ] Create `ListenerSubscriberInterface` diff --git a/src/ListenerProvider/PrioritizedAggregateListenerProvider.php b/src/ListenerProvider/PrioritizedAggregateListenerProvider.php new file mode 100644 index 0000000..fd5de68 --- /dev/null +++ b/src/ListenerProvider/PrioritizedAggregateListenerProvider.php @@ -0,0 +1,82 @@ +validateProviders($providers); + $this->providers = $providers; + } + + /** + * {@inheritDoc} + * @param string[] $identifiers Any identifiers to use when retrieving + * listeners from child providers. + */ + public function getListenersForEvent($event, array $identifiers = []) + { + yield from $this->iterateByPriority( + $this->getListenersForEventByPriority($event, $identifiers) + ); + } + + public function getListenersForEventByPriority($event, array $identifiers = []) + { + $prioritizedListeners = []; + + foreach ($this->providers as $provider) { + foreach ($provider->getListenersForEventByPriority($event, $identifiers) as $priority => $listeners) { + $prioritizedListeners[$priority] = isset($prioritizedListeners[$priority]) + ? array_merge($prioritizedListeners[$priority], $listeners) + : $listeners; + } + } + + return $prioritizedListeners; + } + + /** + * @throws Exception\InvalidArgumentException if any provider is not a + * PrioritizedListenerProviderInterface instance + */ + private function validateProviders(array $providers) + { + foreach ($providers as $index => $provider) { + if (! $provider instanceof PrioritizedListenerProviderInterface) { + throw new Exception\InvalidArgumentException(sprintf( + '%s requires all providers be instances of %s; received provider of type "%s" at index %d', + __CLASS__, + PrioritizedListenerProviderInterface::class, + gettype($provider), + $index + )); + } + } + } + + /** + * @param array $prioritizedListeners + * @return iterable + */ + private function iterateByPriority($prioritizedListeners) + { + krsort($prioritizedListeners); + foreach ($prioritizedListeners as $listeners) { + yield from $listeners; + } + } +} diff --git a/test/ListenerProvider/PrioritizedAggregateListenerProviderTest.php b/test/ListenerProvider/PrioritizedAggregateListenerProviderTest.php new file mode 100644 index 0000000..922d4af --- /dev/null +++ b/test/ListenerProvider/PrioritizedAggregateListenerProviderTest.php @@ -0,0 +1,108 @@ +prophesize(ListenerProviderInterface::class)->reveal(); + return [ + 'null' => [null], + 'true' => [true], + 'false' => [false], + 'zero' => [0], + 'int' => [1], + 'zero-float' => [0.0], + 'float' => [1.1], + 'string' => ['invalid'], + 'array' => [['invalid']], + 'object' => [(object) ['value' => 'invalid']], + 'non-prioritized-provider' => [$genericProvider], + ]; + } + + /** + * @dataProvider invalidProviders + * @param mixed $provider + */ + public function testConstructorRaisesExceptionForInvalidProviders($provider) + { + $this->expectException(Exception\InvalidArgumentException::class); + $this->expectExceptionMessage(PrioritizedListenerProviderInterface::class); + new PrioritizedAggregateListenerProvider([$provider]); + } + + public function testIteratesProvidersInOrderExpected() + { + $event = new Event(); + $event->setName('test'); + + $baseListener = function () { + }; + + $first = clone $baseListener; + $second = clone $baseListener; + $third = clone $baseListener; + $fourth = clone $baseListener; + $fifth = clone $baseListener; + $sixth = clone $baseListener; + $seventh = clone $baseListener; + $eighth = clone $baseListener; + $ninth = clone $baseListener; + + $provider = new PrioritizedListenerProvider(); + $provider->attachWildcardListener($first); + $provider->attach(Event::class, $second); + $provider->attach('test', $third); + + $identifiedProvider = new PrioritizedIdentifierListenerProvider(); + $identifiedProvider->attach(Event::class, '*', $fourth); + $identifiedProvider->attach(Event::class, Event::class, $fifth); + $identifiedProvider->attach(Event::class, 'test', $sixth); + $identifiedProvider->attach('*', '*', $seventh); + $identifiedProvider->attach('*', Event::class, $eighth); + $identifiedProvider->attach('*', 'test', $ninth); + + $aggregateProvider = new PrioritizedAggregateListenerProvider([ + $provider, + $identifiedProvider, + ]); + + $prioritizedListeners = []; + $index = 1; + + foreach ($aggregateProvider->getListenersForEvent($event, [Event::class]) as $listener) { + $prioritizedListeners[$index] = spl_object_hash($listener); + $index += 1; + } + + $expected = [ + 1 => spl_object_hash($third), + 2 => spl_object_hash($second), + 3 => spl_object_hash($first), + 4 => spl_object_hash($sixth), + 5 => spl_object_hash($fifth), + 6 => spl_object_hash($fourth), + 7 => spl_object_hash($ninth), + 8 => spl_object_hash($eighth), + 9 => spl_object_hash($seventh), + ]; + + $this->assertSame($expected, $prioritizedListeners); + } +} From 64ac5030db172fda0b24dfc78730b27f6da1fd5a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 4 Apr 2019 10:40:58 -0500 Subject: [PATCH 12/26] refactor: Have SharedEventManager extend PrioritizedIdentifierListenerProvider Doing so will allow us to use it in a PrioritizedAggregateListenerProvider within the EventManager later. Required a couple changes to tests, as PrioritizedIdentifierListenerProvider widens what are allowed as events and identifiers when retrieving listeners. --- TODO-PSR-14.md | 1 + src/SharedEventManager.php | 218 +------------------------------- test/SharedEventManagerTest.php | 9 +- 3 files changed, 10 insertions(+), 218 deletions(-) diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 9eb4d09..a0f5923 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -45,6 +45,7 @@ - [x] `getListenersByEvent()` will loop through each, in order, calling the `getListenersForEventByPriority()` method of each, returning the aggregated listeners in priority order. + - [x] Make `SharedEventManager` an extension of `PrioritizedIdentifierListenerProvider` - [ ] Create `ListenerSubscriberInterface` - [ ] `attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1)` - [ ] `detach(PrioritizedListenerAttachmentInterface $provider)` diff --git a/src/SharedEventManager.php b/src/SharedEventManager.php index 59e2690..359467f 100644 --- a/src/SharedEventManager.php +++ b/src/SharedEventManager.php @@ -15,220 +15,10 @@ * Allows attaching to EMs composed by other classes without having an instance first. * The assumption is that the SharedEventManager will be injected into EventManager * instances, and then queried for additional listeners when triggering an event. + * + * @deprecated since 3.3.0. This class will be removed in version 4.0; use + * listener providers instead. */ -class SharedEventManager implements SharedEventManagerInterface +class SharedEventManager extends ListenerProvider\PrioritizedIdentifierListenerProvider { - /** - * Identifiers with event connections - * @var array - */ - protected $identifiers = []; - - /** - * Attach a listener to an event emitted by components with specific identifiers. - * - * As an example, the following connects to the "getAll" event of both an - * AbstractResource and EntityResource: - * - * - * $sharedEventManager = new SharedEventManager(); - * foreach (['My\Resource\AbstractResource', 'My\Resource\EntityResource'] as $identifier) { - * $sharedEventManager->attach( - * $identifier, - * 'getAll', - * function ($e) use ($cache) { - * if (!$id = $e->getParam('id', false)) { - * return; - * } - * if (!$data = $cache->load(get_class($resource) . '::getOne::' . $id )) { - * return; - * } - * return $data; - * } - * ); - * } - * - * - * @param string $identifier Identifier for event emitting component. - * @param string $event - * @param callable $listener Listener that will handle the event. - * @param int $priority Priority at which listener should execute - * @return void - * @throws Exception\InvalidArgumentException for invalid identifier arguments. - * @throws Exception\InvalidArgumentException for invalid event arguments. - */ - public function attach($identifier, $event, callable $listener, $priority = 1) - { - if (! is_string($identifier) || empty($identifier)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Invalid identifier provided; must be a string; received "%s"', - (is_object($identifier) ? get_class($identifier) : gettype($identifier)) - )); - } - - if (! is_string($event) || empty($event)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Invalid event provided; must be a non-empty string; received "%s"', - (is_object($event) ? get_class($event) : gettype($event)) - )); - } - - $this->identifiers[$identifier][$event][(int) $priority][] = $listener; - } - - /** - * @inheritDoc - */ - public function detach(callable $listener, $identifier = null, $eventName = null, $force = false) - { - // No identifier or wildcard identifier: loop through all identifiers and detach - if (null === $identifier || ('*' === $identifier && ! $force)) { - foreach (array_keys($this->identifiers) as $identifier) { - $this->detach($listener, $identifier, $eventName, true); - } - return; - } - - if (! is_string($identifier) || empty($identifier)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Invalid identifier provided; must be a string, received %s', - (is_object($identifier) ? get_class($identifier) : gettype($identifier)) - )); - } - - // Do we have any listeners on the provided identifier? - if (! isset($this->identifiers[$identifier])) { - return; - } - - if (null === $eventName || ('*' === $eventName && ! $force)) { - foreach (array_keys($this->identifiers[$identifier]) as $eventName) { - $this->detach($listener, $identifier, $eventName, true); - } - return; - } - - if (! is_string($eventName) || empty($eventName)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Invalid event name provided; must be a string, received %s', - (is_object($eventName) ? get_class($eventName) : gettype($eventName)) - )); - } - - if (! isset($this->identifiers[$identifier][$eventName])) { - return; - } - - foreach ($this->identifiers[$identifier][$eventName] as $priority => $listeners) { - foreach ($listeners as $index => $evaluatedListener) { - if ($evaluatedListener !== $listener) { - continue; - } - - // Found the listener; remove it. - unset($this->identifiers[$identifier][$eventName][$priority][$index]); - - // Is the priority queue empty? - if (empty($this->identifiers[$identifier][$eventName][$priority])) { - unset($this->identifiers[$identifier][$eventName][$priority]); - break; - } - } - - // Is the event queue empty? - if (empty($this->identifiers[$identifier][$eventName])) { - unset($this->identifiers[$identifier][$eventName]); - break; - } - } - - // Is the identifier queue now empty? Remove it. - if (empty($this->identifiers[$identifier])) { - unset($this->identifiers[$identifier]); - } - } - - /** - * Retrieve all listeners for a given identifier and event - * - * @param string[] $identifiers - * @param string $eventName - * @return array[] - * @throws Exception\InvalidArgumentException - */ - public function getListeners(array $identifiers, $eventName) - { - if ('*' === $eventName || ! is_string($eventName) || empty($eventName)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Event name passed to %s must be a non-empty, non-wildcard string', - __METHOD__ - )); - } - - $returnListeners = []; - - foreach ($identifiers as $identifier) { - if ('*' === $identifier || ! is_string($identifier) || empty($identifier)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Identifier names passed to %s must be non-empty, non-wildcard strings', - __METHOD__ - )); - } - - if (isset($this->identifiers[$identifier])) { - $listenersByIdentifier = $this->identifiers[$identifier]; - if (isset($listenersByIdentifier[$eventName])) { - foreach ($listenersByIdentifier[$eventName] as $priority => $listeners) { - $returnListeners[$priority][] = $listeners; - } - } - if (isset($listenersByIdentifier['*'])) { - foreach ($listenersByIdentifier['*'] as $priority => $listeners) { - $returnListeners[$priority][] = $listeners; - } - } - } - } - - if (isset($this->identifiers['*'])) { - $wildcardIdentifier = $this->identifiers['*']; - if (isset($wildcardIdentifier[$eventName])) { - foreach ($wildcardIdentifier[$eventName] as $priority => $listeners) { - $returnListeners[$priority][] = $listeners; - } - } - if (isset($wildcardIdentifier['*'])) { - foreach ($wildcardIdentifier['*'] as $priority => $listeners) { - $returnListeners[$priority][] = $listeners; - } - } - } - - foreach ($returnListeners as $priority => $listOfListeners) { - $returnListeners[$priority] = array_merge(...$listOfListeners); - } - - return $returnListeners; - } - - /** - * @inheritDoc - */ - public function clearListeners($identifier, $eventName = null) - { - if (! isset($this->identifiers[$identifier])) { - return false; - } - - if (null === $eventName) { - unset($this->identifiers[$identifier]); - return; - } - - if (! isset($this->identifiers[$identifier][$eventName])) { - return; - } - - unset($this->identifiers[$identifier][$eventName]); - } } diff --git a/test/SharedEventManagerTest.php b/test/SharedEventManagerTest.php index 9ec783e..a68639a 100644 --- a/test/SharedEventManagerTest.php +++ b/test/SharedEventManagerTest.php @@ -284,15 +284,16 @@ public function testDetachingWithInvalidEventTypeRaisesException($eventName) $this->manager->detach($this->callback, 'IDENTIFIER', $eventName); } - public function invalidListenersAndEventNamesForFetchingListeners() + public function invalidEventNamesForFetchingListeners() { $events = $this->invalidIdentifiers(); $events['wildcard'] = ['*']; + unset($events['non-traversable-object']); return $events; } /** - * @dataProvider invalidListenersAndEventNamesForFetchingListeners + * @dataProvider invalidEventNamesForFetchingListeners */ public function testGetListenersRaisesExceptionForInvalidEventName($eventName) { @@ -302,12 +303,12 @@ public function testGetListenersRaisesExceptionForInvalidEventName($eventName) } /** - * @dataProvider invalidListenersAndEventNamesForFetchingListeners + * @dataProvider invalidIdentifiers */ public function testGetListenersRaisesExceptionForInvalidIdentifier($identifier) { $this->expectException(Exception\InvalidArgumentException::class); - $this->expectExceptionMessage('non-empty, non-wildcard'); + $this->expectExceptionMessage('non-empty'); $this->manager->getListeners([$identifier], 'EVENT'); } } From 06846636d074ebccc60b7e9b39fb429ceb6982c3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 4 Apr 2019 14:10:14 -0500 Subject: [PATCH 13/26] feat: return listener from attach, attachWildcardListener methods This is necessary to keep feature parity with current versions, but can be removed in version 4. --- .../PrioritizedListenerAttachmentInterface.php | 12 ++++++++++-- src/ListenerProvider/PrioritizedListenerProvider.php | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ListenerProvider/PrioritizedListenerAttachmentInterface.php b/src/ListenerProvider/PrioritizedListenerAttachmentInterface.php index 40cfbd5..e6e3f9f 100644 --- a/src/ListenerProvider/PrioritizedListenerAttachmentInterface.php +++ b/src/ListenerProvider/PrioritizedListenerAttachmentInterface.php @@ -14,7 +14,11 @@ interface PrioritizedListenerAttachmentInterface * @param callable $listener The listener itself. * @param int $priority The priority at which to attach the listener. High * priorities respond earlier; negative priorities respond later. - * @return void + * @return callable The listener attached, to allow subscribers to track + * which listeners were attached, and thus detach them. This return + * value will be changed to `void` in version 4; we recommend + * subscribers write their own logic for tracking what has and hasn't + * been attached. */ public function attach($event, callable $listener, $priority = 1); @@ -41,7 +45,11 @@ public function detach(callable $listener, $event = null); * @param callable $listener The listener to attach. * @param int $priority The priority at which to attach the listener. * High priorities respond earlier; negative priorities respond later. - * @return void + * @return callable The listener attached, to allow subscribers to track + * which listeners were attached, and thus detach them. This return + * value will be changed to `void` in version 4; we recommend + * subscribers write their own logic for tracking what has and hasn't + * been attached. */ public function attachWildcardListener(callable $listener, $priority = 1); diff --git a/src/ListenerProvider/PrioritizedListenerProvider.php b/src/ListenerProvider/PrioritizedListenerProvider.php index 8f8acee..4f8635d 100644 --- a/src/ListenerProvider/PrioritizedListenerProvider.php +++ b/src/ListenerProvider/PrioritizedListenerProvider.php @@ -97,6 +97,8 @@ public function attach($event, callable $listener, $priority = 1) } $this->events[$event][(int) $priority][0][] = $listener; + + return $listener; } /** @@ -153,6 +155,7 @@ public function detach(callable $listener, $event = null, $force = false) public function attachWildcardListener(callable $listener, $priority = 1) { $this->events['*'][(int) $priority][0][] = $listener; + return $listener; } /** From 109304176b153a3de36b8b5e038efec176b405e6 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 4 Apr 2019 11:02:14 -0500 Subject: [PATCH 14/26] feat: adds a ListenerSubscriberInterface Added to the ListenerProvider namespace. Accepts a PrioritizedListenerAttachmentInterface argument, to which it will subscribe listeners. --- TODO-PSR-14.md | 6 ++--- .../ListenerSubscriberInterface.php | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/ListenerProvider/ListenerSubscriberInterface.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index a0f5923..2adf962 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -46,9 +46,9 @@ `getListenersForEventByPriority()` method of each, returning the aggregated listeners in priority order. - [x] Make `SharedEventManager` an extension of `PrioritizedIdentifierListenerProvider` - - [ ] Create `ListenerSubscriberInterface` - - [ ] `attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1)` - - [ ] `detach(PrioritizedListenerAttachmentInterface $provider)` + - [x] Create `ListenerSubscriberInterface` + - [x] `attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1)` + - [x] `detach(PrioritizedListenerAttachmentInterface $provider)` - [ ] Create `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait` - [ ] define a default `detach()` implementation - [ ] Create `LazyListenerSubscriber` based on `LazyListenerAggregate` diff --git a/src/ListenerProvider/ListenerSubscriberInterface.php b/src/ListenerProvider/ListenerSubscriberInterface.php new file mode 100644 index 0000000..51b40cb --- /dev/null +++ b/src/ListenerProvider/ListenerSubscriberInterface.php @@ -0,0 +1,22 @@ + Date: Thu, 4 Apr 2019 11:06:26 -0500 Subject: [PATCH 15/26] feat: Provides AbstractListenerSubscriber and ListenerSubscriberTrait Each implements ListenerSubscriberInterface::detach --- TODO-PSR-14.md | 4 +- .../AbstractListenerSubscriber.php | 27 ++++++ .../ListenerSubscriberTrait.php | 30 +++++++ .../AbstractListenerSubscriberTest.php | 37 ++++++++ .../ListenerSubscriberTraitTest.php | 90 +++++++++++++++++++ 5 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/ListenerProvider/AbstractListenerSubscriber.php create mode 100644 src/ListenerProvider/ListenerSubscriberTrait.php create mode 100644 test/ListenerProvider/AbstractListenerSubscriberTest.php create mode 100644 test/ListenerProvider/ListenerSubscriberTraitTest.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 2adf962..7676a5c 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -49,8 +49,8 @@ - [x] Create `ListenerSubscriberInterface` - [x] `attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1)` - [x] `detach(PrioritizedListenerAttachmentInterface $provider)` - - [ ] Create `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait` - - [ ] define a default `detach()` implementation + - [x] Create `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait` + - [x] define a default `detach()` implementation - [ ] Create `LazyListenerSubscriber` based on `LazyListenerAggregate` - [ ] Define an alternate LazyListener: - [ ] `__construct(ContainerInterface $container, string $event = null, int $priority = 1)` diff --git a/src/ListenerProvider/AbstractListenerSubscriber.php b/src/ListenerProvider/AbstractListenerSubscriber.php new file mode 100644 index 0000000..1b8637b --- /dev/null +++ b/src/ListenerProvider/AbstractListenerSubscriber.php @@ -0,0 +1,27 @@ +listeners as $index => $callback) { + $provider->detach($callback); + unset($this->listeners[$index]); + } + } +} diff --git a/src/ListenerProvider/ListenerSubscriberTrait.php b/src/ListenerProvider/ListenerSubscriberTrait.php new file mode 100644 index 0000000..de0e1b1 --- /dev/null +++ b/src/ListenerProvider/ListenerSubscriberTrait.php @@ -0,0 +1,30 @@ +listeners as $index => $callback) { + $provider->detach($callback); + unset($this->listeners[$index]); + } + } +} diff --git a/test/ListenerProvider/AbstractListenerSubscriberTest.php b/test/ListenerProvider/AbstractListenerSubscriberTest.php new file mode 100644 index 0000000..e474e2d --- /dev/null +++ b/test/ListenerProvider/AbstractListenerSubscriberTest.php @@ -0,0 +1,37 @@ +attachmentCallback = $attachmentCallback; + } + + public function attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1) + { + $attachmentCallback = $this->attachmentCallback->bindTo($this, $this); + $attachmentCallback($provider, $priority); + } + }; + } +} diff --git a/test/ListenerProvider/ListenerSubscriberTraitTest.php b/test/ListenerProvider/ListenerSubscriberTraitTest.php new file mode 100644 index 0000000..f98d4a1 --- /dev/null +++ b/test/ListenerProvider/ListenerSubscriberTraitTest.php @@ -0,0 +1,90 @@ +attachmentCallback = $attachmentCallback; + } + + public function attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1) + { + $attachmentCallback = $this->attachmentCallback->bindTo($this, $this); + $attachmentCallback($provider, $priority); + } + }; + } + + public function testSubscriberAttachesListeners() + { + $baseListener = function () { + }; + $listener1 = clone $baseListener; + $listener2 = clone $baseListener; + $listener3 = clone $baseListener; + + $provider = $this->prophesize(PrioritizedListenerAttachmentInterface::class); + $provider->attach('foo.bar', $listener1, 100)->will(function ($args) { + return $args[1]; + }); + $provider->attach('foo.baz', $listener2, 100)->will(function ($args) { + return $args[1]; + }); + + $subscriber = $this->createProvider(function ($provider, $priority) use ($listener1, $listener2) { + $this->listeners[] = $provider->attach('foo.bar', $listener1, $priority); + $this->listeners[] = $provider->attach('foo.baz', $listener2, $priority); + }); + + $subscriber->attach($provider->reveal(), 100); + + $this->assertAttributeSame([$listener1, $listener2], 'listeners', $subscriber); + + return [ + 'subscriber' => $subscriber, + 'provider' => $provider, + 'listener1' => $listener1, + 'listener2' => $listener2, + ]; + } + + /** + * @depends testSubscriberAttachesListeners + * @param array $dependencies + */ + public function testDetachRemovesAttachedListeners(array $dependencies) + { + $subscriber = $dependencies['subscriber']; + $provider = $dependencies['provider']; + + $provider->detach($dependencies['listener1'])->shouldBeCalledTimes(1); + $provider->detach($dependencies['listener2'])->shouldBeCalledTimes(1); + + $subscriber->detach($provider->reveal()); + $this->assertAttributeSame([], 'listeners', $subscriber); + } +} From b52c547f79d0ab93c2def134efe95b162eb11954 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 4 Apr 2019 17:09:52 -0500 Subject: [PATCH 16/26] feat: Implement lazy listeners and lazy listener subscriber Combines the features of LazyListener and LazyEventListener into `Zend\EventManager\ListenerProvider\LazyListener`. `LazyListenerSubscriber` is based on `LazyListenerAggregate`, but simplifies it by having it compose `LazyListener` instances only (no creation within it). --- TODO-PSR-14.md | 17 +- src/ListenerProvider/LazyListener.php | 156 ++++++++++++++++ .../LazyListenerSubscriber.php | 102 +++++++++++ .../LazyListenerSubscriberTest.php | 92 ++++++++++ test/ListenerProvider/LazyListenerTest.php | 167 ++++++++++++++++++ 5 files changed, 526 insertions(+), 8 deletions(-) create mode 100644 src/ListenerProvider/LazyListener.php create mode 100644 src/ListenerProvider/LazyListenerSubscriber.php create mode 100644 test/ListenerProvider/LazyListenerSubscriberTest.php create mode 100644 test/ListenerProvider/LazyListenerTest.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 7676a5c..1c37031 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -51,15 +51,16 @@ - [x] `detach(PrioritizedListenerAttachmentInterface $provider)` - [x] Create `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait` - [x] define a default `detach()` implementation - - [ ] Create `LazyListenerSubscriber` based on `LazyListenerAggregate` - - [ ] Define an alternate LazyListener: - - [ ] `__construct(ContainerInterface $container, string $event = null, int $priority = 1)` - - [ ] implements functionality from both `LazyListener` and `LazyEventListener`, minus passing env to container - - [ ] without an event, can be attached to any provider - - [ ] with an event, can be attached to `LazyListenerSubscriber` - - Constructor aggregates `LazyListener` _instances_ only - - [ ] `attach()` skips any where `getEvent()` returns null + - [x] Create `LazyListenerSubscriber` based on `LazyListenerAggregate` + - [x] Define an alternate LazyListener: + - [x] `__construct(ContainerInterface $container, string $event = null, int $priority = 1)` + - [x] implements functionality from both `LazyListener` and `LazyEventListener`, minus passing env to container + - [x] without an event, can be attached to any provider + - [x] with an event, can be attached to `LazyListenerSubscriber` + - [x] Constructor aggregates `LazyListener` _instances_ only + - [x] raises exception when `getEvent()` returns null - [ ] Event Dispatcher implementation + - [ ] Implement `PrioritizedListenerAttachmentInterface` (if BC) - [ ] Create a `PrioritizedListenerProvider` instance in the `EventManger` constructor, and have the various `attach()`, `detach()`, etc. methods proxy to it. diff --git a/src/ListenerProvider/LazyListener.php b/src/ListenerProvider/LazyListener.php new file mode 100644 index 0000000..45acf8b --- /dev/null +++ b/src/ListenerProvider/LazyListener.php @@ -0,0 +1,156 @@ +container = $container; + $this->service = $listener; + $this->method = $method; + $this->event = $event; + $this->priority = $priority; + } + + /** + * Use the listener as an invokable, allowing direct attachment to an event manager. + * + * @param object $event + * @return void + */ + public function __invoke($event) + { + $listener = $this->fetchListener(); + $method = $this->method; + $listener->{$method}($event); + } + + /** + * @return null|string + */ + public function getEvent() + { + return $this->event; + } + + /** + * Return the priority, or, if not set, the default provided. + * + * @param int $default + * @return int + */ + public function getPriority($default = 1) + { + return null !== $this->priority ? (int) $this->priority : (int) $default; + } + + /** + * @return callable + */ + private function fetchListener() + { + if ($this->listener) { + return $this->listener; + } + + $this->listener = $this->container->get($this->service); + + return $this->listener; + } +} diff --git a/src/ListenerProvider/LazyListenerSubscriber.php b/src/ListenerProvider/LazyListenerSubscriber.php new file mode 100644 index 0000000..bf8c6f6 --- /dev/null +++ b/src/ListenerProvider/LazyListenerSubscriber.php @@ -0,0 +1,102 @@ + + * $subscriber = new LazyListenerSubscriber($listOfLazyListeners); + * $subscriber->attach($provider, $priority); + * )); + * + */ +class LazyListenerSubscriber implements ListenerSubscriberInterface +{ + /** + * LazyListener instances. + * + * @var LazyListener[] + */ + private $listeners = []; + + /** + * @throws Exception\InvalidArgumentException if any member of $listeners + * is not a LazyListener instance. + * @throws Exception\InvalidArgumentException if any member of $listeners + * does not have a defined event to which to attach. + */ + public function __construct(array $listeners) + { + $this->validateListeners($listeners); + $this->listeners = $listeners; + } + + /** + * Subscribe listeners to the provider. + * + * Loops through all composed lazy listeners, and attaches them to the + * provider. + */ + public function attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1) + { + foreach ($this->listeners as $listener) { + $provider->attach( + $listener->getEvent(), + $listener, + $listener->getPriority($priority) + ); + } + } + + public function detach(PrioritizedListenerAttachmentInterface $provider) + { + foreach ($this->listeners as $listener) { + $provider->detach($listener, $listener->getEvent()); + } + } + + /** + * @throws Exception\InvalidArgumentException if any member of $listeners + * is not a LazyListener instance. + * @throws Exception\InvalidArgumentException if any member of $listeners + * does not have a defined event to which to attach. + */ + private function validateListeners(array $listeners) + { + foreach ($listeners as $index => $listener) { + if (! $listener instanceof LazyListener) { + throw new Exception\InvalidArgumentException(sprintf( + '%s only accepts %s instances; received listener of type %s at index %s', + __CLASS__, + LazyListener::class, + gettype($listener), + $index + )); + } + + if (null === $listener->getEvent()) { + throw new Exception\InvalidArgumentException(sprintf( + '%s requires that all %s instances compose a non-empty string event to which to attach;' + . ' none provided for listener at index %s', + __CLASS__, + LazyListener::class, + $index + )); + } + } + } +} diff --git a/test/ListenerProvider/LazyListenerSubscriberTest.php b/test/ListenerProvider/LazyListenerSubscriberTest.php new file mode 100644 index 0000000..9a788b6 --- /dev/null +++ b/test/ListenerProvider/LazyListenerSubscriberTest.php @@ -0,0 +1,92 @@ +container = $this->prophesize(ContainerInterface::class); + } + + public function invalidListenerTypes() + { + return [ + 'null' => [null], + 'true' => [true], + 'false' => [false], + 'zero' => [0], + 'int' => [1], + 'zero-float' => [0.0], + 'float' => [1.1], + 'string' => ['listener'], + 'array' => [['listener']], + 'object' => [(object) ['event' => 'event', 'listener' => 'listener', 'method' => 'method']], + ]; + } + + /** + * @dataProvider invalidListenerTypes + */ + public function testPassingInvalidListenerTypesAtInstantiationRaisesException($listener) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('only accepts ' . LazyListener::class . ' instances'); + new LazyListenerSubscriber([$listener]); + } + + public function testPassingLazyListenersMissingAnEventAtInstantiationRaisesException() + { + $listener = $this->prophesize(LazyListener::class); + $listener->getEvent()->willReturn(null); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('compose a non-empty string event'); + new LazyListenerSubscriber([$listener->reveal()]); + } + + public function testAttachesLazyListenersToProviderUsingEventAndPriority() + { + $listener = $this->prophesize(LazyListener::class); + $listener->getEvent()->willReturn('test'); + $listener->getPriority(1000)->willReturn(100); + + $subscriber = new LazyListenerSubscriber([$listener->reveal()]); + + $provider = $this->prophesize(PrioritizedListenerAttachmentInterface::class); + $provider->attach('test', $listener->reveal(), 100)->shouldBeCalledTimes(1); + + $this->assertNull($subscriber->attach($provider->reveal(), 1000)); + + return [ + 'listener' => $listener, + 'subscriber' => $subscriber, + 'provider' => $provider, + ]; + } + + /** + * @depends testAttachesLazyListenersToProviderUsingEventAndPriority + */ + public function testDetachesLazyListenersFromProviderUsingEvent(array $dependencies) + { + $listener = $dependencies['listener']; + $subscriber = $dependencies['subscriber']; + $provider = $dependencies['provider']; + + $provider->detach($listener->reveal(), 'test')->shouldBeCalledTimes(1); + $this->assertNull($subscriber->detach($provider->reveal())); + } +} diff --git a/test/ListenerProvider/LazyListenerTest.php b/test/ListenerProvider/LazyListenerTest.php new file mode 100644 index 0000000..d68639b --- /dev/null +++ b/test/ListenerProvider/LazyListenerTest.php @@ -0,0 +1,167 @@ +container = $this->prophesize(ContainerInterface::class); + } + + public function invalidListenerTypes() + { + return [ + 'null' => [null], + 'true' => [true], + 'false' => [false], + 'zero' => [0], + 'int' => [1], + 'zero-float' => [0.0], + 'float' => [1.1], + 'empty' => [''], + 'array' => [['event']], + 'object' => [(object) ['event' => 'event']], + ]; + } + + /** + * @dataProvider invalidListenerTypes + * @param mixed $listener + */ + public function testConstructorRaisesExceptionForInvalidListenerType($listener) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('requires a non-empty string $listener argument'); + new LazyListener($this->container->reveal(), $listener); + } + + public function invalidMethodArguments() + { + return array_merge($this->invalidListenerTypes(), [ + 'digit-first' => ['0invalid'], + 'with-whitespace' => ['also invalid'], + 'with-dash' => ['also-invalid'], + 'with-symbols' => ['alsoInv@l!d'], + ]); + } + + /** + * @dataProvider invalidMethodArguments + * @param mixed $method + */ + public function testConstructorRaisesExceptionForInvalidMethodArgument($method) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('requires a valid string $method argument'); + new LazyListener($this->container->reveal(), 'valid-listener-name', $method); + } + + public function invalidEventArguments() + { + $types = $this->invalidListenerTypes(); + unset($types['null']); + return $types; + } + + /** + * @dataProvider invalidEventArguments + * @param mixed $event + */ + public function testConstructorRaisesExceptionForInvalidEventArgument($event) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('requires a null or non-empty string $event argument'); + new LazyListener($this->container->reveal(), 'valid-listener-name', '__invoke', $event); + } + + public function testGetEventReturnsNullWhenNoEventProvidedToConstructor() + { + $listener = new LazyListener($this->container->reveal(), 'valid-listener-name'); + $this->assertNull($listener->getEvent()); + } + + public function testGetEventReturnsEventNameWhenEventProvidedToConstructor() + { + $listener = new LazyListener($this->container->reveal(), 'valid-listener-name', '__invoke', 'test'); + $this->assertEquals('test', $listener->getEvent()); + } + + public function testGetPriorityReturnsPriorityDefaultWhenNoPriorityProvidedToConstructor() + { + $listener = new LazyListener($this->container->reveal(), 'valid-listener-name'); + $this->assertEquals(100, $listener->getPriority(100)); + } + + public function testGetPriorityReturnsIntegerPriorityValueWhenPriorityProvidedToConstructor() + { + $listener = new LazyListener($this->container->reveal(), 'valid-listener-name', '__invoke', 'test', 100); + $this->assertEquals(100, $listener->getPriority()); + } + + public function testGetPriorityReturnsIntegerPriorityValueWhenPriorityProvidedToConstructorAndToMethod() + { + $listener = new LazyListener($this->container->reveal(), 'valid-listener-name', '__invoke', 'test', 100); + $this->assertEquals(100, $listener->getPriority(1000)); + } + + public function methodsToInvoke() + { + return [ + '__invoke' => ['__invoke', '__invoke'], + 'run' => ['run', 'run'], + 'onEvent' => ['onEvent', 'onEvent'], + ]; + } + + /** + * @dataProvider methodsToInvoke + * @param string $method + * @param string $expected + */ + public function testInvocationInvokesMethodDefinedInListener($method, $expected) + { + $listener = new class { + public function __invoke($e) + { + $e->value = __FUNCTION__; + } + + public function run($e) + { + $e->value = __FUNCTION__; + } + + public function onEvent($e) + { + $e->value = __FUNCTION__; + } + }; + + $this->container + ->get('listener') + ->willReturn($listener) + ->shouldBeCalledTimes(1); + + $event = (object) ['value' => null]; + + $lazyListener = new LazyListener($this->container->reveal(), 'listener', $method); + + $lazyListener($event); + + $this->assertEquals($expected, $event->value); + } +} From efec2c028bf6e6105e54faee93b418b36b9ca0c6 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 8 Apr 2019 14:12:50 -0500 Subject: [PATCH 17/26] feat: update EventManager to implement ListenerProviderInterface and PrioritizedListenerAttachmentInterface Allows the EventManager to act as its own provider. --- src/EventManager.php | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/EventManager.php b/src/EventManager.php index bb2a6c0..9ecaaba 100644 --- a/src/EventManager.php +++ b/src/EventManager.php @@ -17,7 +17,10 @@ * Use the EventManager when you want to create a per-instance notification * system for your objects. */ -class EventManager implements EventManagerInterface +class EventManager implements + EventManagerInterface, + ListenerProvider\ListenerProviderInterface, + ListenerProvider\PrioritizedListenerAttachmentInterface { /** * Subscribed events and their listeners @@ -196,6 +199,13 @@ public function attach($eventName, callable $listener, $priority = 1) return $listener; } + /** + * @inheritDoc + */ + public function attachWildcardListener(callable $listener, $priority = 1) + { + } + /** * @inheritDoc * @throws Exception\InvalidArgumentException for invalid event types. @@ -246,6 +256,13 @@ public function detach(callable $listener, $eventName = null, $force = false) } } + /** + * @inheritDoc + */ + public function detachWildcardListener(callable $listener) + { + } + /** * @inheritDoc */ @@ -256,6 +273,13 @@ public function clearListeners($eventName) } } + /** + * @inheritDoc + */ + public function getListenersForEvent($event) + { + } + /** * Prepare arguments * From c1f4cd598c67212203caf7afca91122f1b120efe Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 9 Apr 2019 16:07:02 -0500 Subject: [PATCH 18/26] feat: update EventManager to use listener providers - Adds `Zend\EventManager\EventDispatcherInterface` as a forwards compatibility shim for PSR-14. - Adds `Zend\EventManager\SharedEventManager\SharedEventManagerDecorator`, which decorates generic `SharedEventManagerInterface` instances as listener providers. - Modifies `PrioritizedAggregateListenerProvider` to accept an optional `ListenerProviderInterface $default` argument. This allows non-prioritized `SharedEventManagerInterface` instances (such as the `SharedEventManagerDecorator` in the previous item) to be fallback providers. - Modifies `Zend\EventManager\EventManager` as follows: - It now implements `EventDispatcherInterface` - It now composes a `$provider` property, and an optional `$prioritizedProvider` property. If you instantiate it per previous versions, it creates a `PrioritizedListenerProvider` instance and assigns it to the `$prioritizedProvider` property. It then checks to see if a shared manager was provided, and the type provided, to either assign the `$prioritizedProvider` as the `$provider`, or a `PrioritizedAggregateListenerProvider` that composes both the `$prioritizedProvider` and shared manager instances. - It adds a static named constructor, `createUsingListenerProvider()`, which accepts a single `ListenerProviderInterface` instance. This value is assigned to `$provider`, and, if it is a `PrioritizedListenerAttachmentInterface` instance, to the `$prioritizedProvider` property as well. - Each of the listener attachment methods (attach, detach, clearListeners, *WildcardListeners) now proxy to the composed `$prioritizedProvider`, if any. If there is none, theses methods now raise an exception. - The `getListenersForEvent()` method now proxies to the underling `$provider` property. - The `triggerListeners()` method now consumes the value of `getListenersForEvent()`. - It adds the method `dispatch($event)`, which proxies to `triggerListeners()`, and returns the `$event` it was passed. The method raises an exception of `$event` is a non-object. - Each of `trigger()`, `triggerUntil`, `triggerEvent`, `triggerEventUntil`, `getIdentifiers`, `setIdentifiers`, `addIdenitifers`, `getSharedManager`, `attach`, `detach`, `attachWildcardListener`, `detachWildcardListener`, `clearListeners`, and `getListenersForEvent` have been marked deprecated. - Updates `EventListenerIntrospectionTrait` to work with the new internals of the `EventManager`. - Updates `EventManagerTest`: - updates `getListenersForEvent()` to work with the new `EventManager` internals - Removes `testAttachShouldAddEventIfItDoesNotExist` as it was irrelevant even before the changes. - Removes the `testTriggeringAnEventWithAnEmptyNameRaisesAnException` test, as this is no longer true; you can use any object as an event now. - Modifies a few tests where they were accessing internal structures that have changed, while keeping the same assertions in place. - Adds `EventManagerWithProviderTest` to demonstrate usage when creating an `EventManager` via its `createUsingListenerProvider()` method. - Updates `EventListenerIntrospectionTraitTest` to work with the new internals of the `EventManager`. --- TODO-PSR-14.md | 36 +- src/EventDispatcherInterface.php | 28 ++ src/EventManager.php | 336 ++++++++++++------ .../PrioritizedAggregateListenerProvider.php | 12 +- .../SharedEventManagerDecorator.php | 95 +++++ src/Test/EventListenerIntrospectionTrait.php | 37 +- test/EventManagerTest.php | 89 ++--- test/EventManagerWithProviderTest.php | 114 ++++++ .../EventListenerIntrospectionTraitTest.php | 6 - 9 files changed, 541 insertions(+), 212 deletions(-) create mode 100644 src/EventDispatcherInterface.php create mode 100644 src/SharedEventManager/SharedEventManagerDecorator.php create mode 100644 test/EventManagerWithProviderTest.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 1c37031..2ec4720 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -59,22 +59,34 @@ - [x] with an event, can be attached to `LazyListenerSubscriber` - [x] Constructor aggregates `LazyListener` _instances_ only - [x] raises exception when `getEvent()` returns null -- [ ] Event Dispatcher implementation - - [ ] Implement `PrioritizedListenerAttachmentInterface` (if BC) - - [ ] Create a `PrioritizedListenerProvider` instance in the `EventManger` - constructor, and have the various `attach()`, `detach()`, etc. methods - proxy to it. - - [ ] When triggering listeners, create a `PrioritizedAggregateListenerProvider` +- [x] Adapter for SharedEventManagerInterface + Since we type-hint on SharedEventManagerInterface, we need to adapt generic + implementations to work as ListenerProviders. + - [x] Class that adapts SharedEventManagerInterface instances to ListenerProviders +- [x] Event Dispatcher implementation + - [x] Implement `PrioritizedListenerAttachmentInterface` (if BC) + - [x] Implement `ListenerProviderInterface` (if BC) + - [x] Create a `PrioritizedListenerProvider` instance in the `EventManger` + constructor + - [x] Decorate it in a `PrioritizedAggregateListenerProvider` + - [x] Have the various `attach()`, `detach()`, etc. methods proxy to it. + - [x] Adapt any provided `SharedEventManagerInterface` instance, and add it + to the `PrioritizedAggregateListenerProvider` + - [x] Create a named constructor that accepts a listener provider and which + then uses it internally. + - [x] If the instance is a `PrioritizedListenerAttachmentInterface` + instance, allow the attach/detach/clear methods to proxy to it. + - [x] When triggering listeners, create a `PrioritizedAggregateListenerProvider` with the composed `PrioritizedListenerProvider` and `SharedListenerProvider` / `PrioritizedIdentifierListenerProvider` implementations, in that order. - - [ ] Replace logic of `triggerListeners()` to just call + - [x] Replace logic of `triggerListeners()` to just call `getListenersForEvent()` on the provider. It can continue to aggregate the responses in a `ResponseCollection` - - [ ] `triggerListeners()` no longer needs to type-hint its first argument - - [ ] Create a `dispatch()` method - - [ ] Method will act like `triggerEvent()`, except - - [ ] it will return the event itself - - [ ] it will need to validate that it received an object before calling + - [x] `triggerListeners()` no longer needs to type-hint its first argument + - [x] Create a `dispatch()` method + - [x] Method will act like `triggerEvent()`, except + - [x] it will return the event itself + - [x] it will need to validate that it received an object before calling `triggerListeners` - [ ] Additional utilities - [ ] `EventDispatchingInterface` with a `getEventDispatcher()` method diff --git a/src/EventDispatcherInterface.php b/src/EventDispatcherInterface.php new file mode 100644 index 0000000..ff4956a --- /dev/null +++ b/src/EventDispatcherInterface.php @@ -0,0 +1,28 @@ +provider = $provider; + if ($provider instanceof ListenerProvider\PrioritizedListenerAttachmentInterface) { + $instance->prioritizedProvider = $provider; + } + return $instance; + } + /** * Constructor * @@ -72,19 +114,34 @@ class EventManager implements * * @param SharedEventManagerInterface $sharedEventManager * @param array $identifiers + * @param bool $skipProviderCreation Internal; used by + * createUsingListenerProvider to ensure that no provider is created during + * instantiation. */ - public function __construct(SharedEventManagerInterface $sharedEventManager = null, array $identifiers = []) + public function __construct(SharedEventManagerInterface $sharedEventManager = null, array $identifiers = [], $skipProviderCreation = false) { + $this->eventPrototype = new Event(); + + if ($skipProviderCreation) { + // Nothing else to do. + return; + } + if ($sharedEventManager) { - $this->sharedManager = $sharedEventManager; + $this->sharedManager = $sharedEventManager instanceof SharedEventManager + ? $sharedEventManager + : new SharedEventManager\SharedEventManagerDecorator($sharedEventManager); $this->setIdentifiers($identifiers); } - $this->eventPrototype = new Event(); + $this->prioritizedProvider = new ListenerProvider\PrioritizedListenerProvider(); + + $this->provider = $this->createProvider($this->prioritizedProvider, $this->sharedManager); } /** - * @inheritDoc + * @deprecated Will be removed in version 4; use event instances when triggering + * events instead. */ public function setEventPrototype(EventInterface $prototype) { @@ -94,6 +151,8 @@ public function setEventPrototype(EventInterface $prototype) /** * Retrieve the shared event manager, if composed. * + * @deprecated Will be removed in version 4; use a listener provider and + * lazy listeners instead. * @return null|SharedEventManagerInterface $sharedEventManager */ public function getSharedManager() @@ -102,7 +161,9 @@ public function getSharedManager() } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated Will be removed in version 4; use fully qualified event names + * and the object inheritance hierarchy instead. */ public function getIdentifiers() { @@ -110,7 +171,9 @@ public function getIdentifiers() } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated Will be removed in version 4; use fully qualified event names + * and the object inheritance hierarchy instead. */ public function setIdentifiers(array $identifiers) { @@ -118,7 +181,9 @@ public function setIdentifiers(array $identifiers) } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated Will be removed in version 4; use fully qualified event names + * and the object inheritance hierarchy instead. */ public function addIdentifiers(array $identifiers) { @@ -129,7 +194,9 @@ public function addIdentifiers(array $identifiers) } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated Will be removed in version 4; use dispatch() with an event + * instance instead. */ public function trigger($eventName, $target = null, $argv = []) { @@ -148,7 +215,10 @@ public function trigger($eventName, $target = null, $argv = []) } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated Will be removed in version 4; use dispatch() with an event + * instance instead, and encapsulate logic for stopping propagation + * within the event itself. */ public function triggerUntil(callable $callback, $eventName, $target = null, $argv = []) { @@ -167,7 +237,8 @@ public function triggerUntil(callable $callback, $eventName, $target = null, $ar } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated Will be removed in version 4; use dispatch() instead. */ public function triggerEvent(EventInterface $event) { @@ -175,7 +246,9 @@ public function triggerEvent(EventInterface $event) } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated Will be removed in version 4; use dispatch() instead, and + * encapsulate logic for stopping propagation within the event itself. */ public function triggerEventUntil(callable $callback, EventInterface $event) { @@ -183,101 +256,137 @@ public function triggerEventUntil(callable $callback, EventInterface $event) } /** - * @inheritDoc + * {@inheritDoc} */ - public function attach($eventName, callable $listener, $priority = 1) + public function dispatch($event) { - if (! is_string($eventName)) { + if (! is_object($event)) { throw new Exception\InvalidArgumentException(sprintf( - '%s expects a string for the event; received %s', - __METHOD__, - (is_object($eventName) ? get_class($eventName) : gettype($eventName)) + '%s expects an object; received "%s"', + __CLASS__, + gettype($event) )); } - $this->events[$eventName][(int) $priority][0][] = $listener; + $this->triggerListeners($event); + return $event; + } + + /** + * {@inheritDoc} + * @deprecated This method will be removed in version 4.0; use listener + * providers and the createUsingListenerProvider method instead. + * @throws Exception\RuntimeException if no prioritized provider is composed. + */ + public function attach($eventName, callable $listener, $priority = 1) + { + if (! $this->prioritizedProvider) { + throw new Exception\RuntimeException(sprintf( + 'The provider composed into this %s instance is not of type %s (received %s);' + . ' attach listeners to it directly using its API before passing to the %s constructor', + get_class($this), + ListenerProvider\PrioritizedListenerAttachmentInterface::class, + gettype($this->provider), + get_class($this) + )); + } + + $this->prioritizedProvider->attach($eventName, $listener, $priority); return $listener; } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated This method will be removed in version 4.0; use listener + * providers and the createUsingListenerProvider method instead. */ public function attachWildcardListener(callable $listener, $priority = 1) { + if (! $this->prioritizedProvider) { + throw new Exception\RuntimeException(sprintf( + 'The provider composed into this %s instance is not of type %s (received %s);' + . ' attach wildcared listeners to it directly using its API before passing to the %s constructor', + get_class($this), + ListenerProvider\PrioritizedListenerAttachmentInterface::class, + gettype($this->provider), + get_class($this) + )); + } + + $this->prioritizedProvider->attachWildcardListener($listener, $priority); + return $listener; } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated This method will be removed in version 4.0; use listener + * providers and the createUsingListenerProvider method instead. * @throws Exception\InvalidArgumentException for invalid event types. */ public function detach(callable $listener, $eventName = null, $force = false) { - - // If event is wildcard, we need to iterate through each listeners - if (null === $eventName || ('*' === $eventName && ! $force)) { - foreach (array_keys($this->events) as $eventName) { - $this->detach($listener, $eventName, true); - } - return; - } - - if (! is_string($eventName)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects a string for the event; received %s', - __METHOD__, - (is_object($eventName) ? get_class($eventName) : gettype($eventName)) + if (! $this->prioritizedProvider) { + throw new Exception\RuntimeException(sprintf( + 'The provider composed into this %s instance is not of type %s (received %s);' + . ' detach listeners from it directly using its API', + get_class($this), + ListenerProvider\PrioritizedListenerAttachmentInterface::class, + gettype($this->provider) )); } - if (! isset($this->events[$eventName])) { - return; - } - - foreach ($this->events[$eventName] as $priority => $listeners) { - foreach ($listeners[0] as $index => $evaluatedListener) { - if ($evaluatedListener !== $listener) { - continue; - } - - // Found the listener; remove it. - unset($this->events[$eventName][$priority][0][$index]); - - // If the queue for the given priority is empty, remove it. - if (empty($this->events[$eventName][$priority][0])) { - unset($this->events[$eventName][$priority]); - break; - } - } - } - - // If the queue for the given event is empty, remove it. - if (empty($this->events[$eventName])) { - unset($this->events[$eventName]); - } + $this->prioritizedProvider->detach($listener, $eventName); } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated This method will be removed in version 4.0; use listener + * providers and the createUsingListenerProvider method instead. */ public function detachWildcardListener(callable $listener) { + if (! $this->prioritizedProvider) { + throw new Exception\RuntimeException(sprintf( + 'The provider composed into this %s instance is not of type %s (received %s);' + . ' detach wildcard listeners from it directly using its API', + get_class($this), + ListenerProvider\PrioritizedListenerAttachmentInterface::class, + gettype($this->provider) + )); + } + + $this->prioritizedProvider->detachWildcardListener($listener); } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated This method will be removed in version 4.0; use listener + * providers and the createUsingListenerProvider method instead. */ public function clearListeners($eventName) { - if (isset($this->events[$eventName])) { - unset($this->events[$eventName]); + if (! $this->prioritizedProvider) { + throw new Exception\RuntimeException(sprintf( + 'The provider composed into this %s instance is not of type %s (received %s);' + . ' clear wildcard listeners from it directly using its API', + get_class($this), + ListenerProvider\PrioritizedListenerAttachmentInterface::class, + gettype($this->provider) + )); } + + $this->prioritizedProvider->clearListeners($eventName); } /** - * @inheritDoc + * {@inheritDoc} + * @deprecated This method will be removed in version 4.0, and EventManager + * will no longer be its own listener provider; use external listener + * providers and the createUsingListenerProvider method instead. */ public function getListenersForEvent($event) { + yield from $this->provider->getListenersForEvent($event, $this->identifiers); } /** @@ -287,6 +396,8 @@ public function getListenersForEvent($event) * listener. It returns an ArrayObject of the arguments, which may then be * passed to trigger(). * + * @deprecated This method will be removed in version 4.0; always use context + * specific events with their own mutation methods. * @param array $args * @return ArrayObject */ @@ -300,70 +411,69 @@ public function prepareArgs(array $args) * * Actual functionality for triggering listeners, to which trigger() delegate. * - * @param EventInterface $event + * @param object $event * @param null|callable $callback * @return ResponseCollection */ - protected function triggerListeners(EventInterface $event, callable $callback = null) + protected function triggerListeners($event, callable $callback = null) { - $name = $event->getName(); - - if (empty($name)) { - throw new Exception\RuntimeException('Event is missing a name; cannot trigger!'); + // Initial value of stop propagation flag should be false + if ($event instanceof EventInterface) { + $event->stopPropagation(false); } - if (isset($this->events[$name])) { - $listOfListenersByPriority = $this->events[$name]; + $stopMethod = $event instanceof StoppableEventInterface ? 'isPropagationStopped' : 'propagationIsStopped'; + + // Execute listeners + $responses = new ResponseCollection(); + + foreach ($this->provider->getListenersForEvent($event, $this->identifiers) as $listener) { + $response = $listener($event); + $responses->push($response); - if (isset($this->events['*'])) { - foreach ($this->events['*'] as $priority => $listOfListeners) { - $listOfListenersByPriority[$priority][] = $listOfListeners[0]; - } + // If the event was asked to stop propagating, do so + if ($event->{$stopMethod}()) { + $responses->setStopped(true); + return $responses; } - } elseif (isset($this->events['*'])) { - $listOfListenersByPriority = $this->events['*']; - } else { - $listOfListenersByPriority = []; - } - if ($this->sharedManager) { - foreach ($this->sharedManager->getListeners($this->identifiers, $name) as $priority => $listeners) { - $listOfListenersByPriority[$priority][] = $listeners; + // If the result causes our validation callback to return true, + // stop propagation + if ($callback && $callback($response)) { + $responses->setStopped(true); + return $responses; } } - // Sort by priority in reverse order - krsort($listOfListenersByPriority); - - // Initial value of stop propagation flag should be false - $event->stopPropagation(false); + return $responses; + } - $stopMethod = $event instanceof StoppableEventInterface ? 'isPropagationStopped' : 'propagationIsStopped'; + /** + * Creates the value for the $provider property, based on the + * $sharedEventManager argument. + * + * @param ListenerProvider\PrioritizedListenerProvider $prioritizedProvider + * @param null|SharedEventManagerInterface $sharedEventManager + * @return ListenerProvider\ListenerProviderInterface + */ + private function createProvider( + ListenerProvider\PrioritizedListenerProvider $prioritizedProvider, + SharedEventManagerInterface $sharedEventManager = null + ) { + if (! $sharedEventManager) { + return $prioritizedProvider; + } - // Execute listeners - $responses = new ResponseCollection(); - foreach ($listOfListenersByPriority as $listOfListeners) { - foreach ($listOfListeners as $listeners) { - foreach ($listeners as $listener) { - $response = $listener($event); - $responses->push($response); - - // If the event was asked to stop propagating, do so - if ($event->{$stopMethod}()) { - $responses->setStopped(true); - return $responses; - } - - // If the result causes our validation callback to return true, - // stop propagation - if ($callback && $callback($response)) { - $responses->setStopped(true); - return $responses; - } - } - } + if ($sharedEventManager instanceof ListenerProvider\PrioritizedListenerProviderInterface) { + return new ListenerProvider\PrioritizedAggregateListenerProvider([ + $prioritizedProvider, + $sharedEventManager, + ]); } - return $responses; + return new ListenerProvider\PrioritizedAggregateListenerProvider( + [$prioritizedProvider], + $sharedEventManager + ); } } diff --git a/src/ListenerProvider/PrioritizedAggregateListenerProvider.php b/src/ListenerProvider/PrioritizedAggregateListenerProvider.php index fd5de68..c6b0b8d 100644 --- a/src/ListenerProvider/PrioritizedAggregateListenerProvider.php +++ b/src/ListenerProvider/PrioritizedAggregateListenerProvider.php @@ -11,15 +11,21 @@ class PrioritizedAggregateListenerProvider implements PrioritizedListenerProviderInterface { + /** + * @var ListenerProviderInterface + */ + private $default; + /** * @var PrioritizedListenerProviderInterface[] */ private $providers; - public function __construct(array $providers) + public function __construct(array $providers, ListenerProviderInterface $default = null) { $this->validateProviders($providers); $this->providers = $providers; + $this->default = $default; } /** @@ -32,6 +38,10 @@ public function getListenersForEvent($event, array $identifiers = []) yield from $this->iterateByPriority( $this->getListenersForEventByPriority($event, $identifiers) ); + + if ($this->default) { + yield from $this->default->getListenersForEvent($event, $identifiers); + } } public function getListenersForEventByPriority($event, array $identifiers = []) diff --git a/src/SharedEventManager/SharedEventManagerDecorator.php b/src/SharedEventManager/SharedEventManagerDecorator.php new file mode 100644 index 0000000..32801cc --- /dev/null +++ b/src/SharedEventManager/SharedEventManagerDecorator.php @@ -0,0 +1,95 @@ +proxy = $proxy; + } + + /** + * {@inheritDoc} + * @var array $identifiers Identifiers provided by dispatcher, if any. + * This argument is deprecated, and will be removed in version 4. + */ + public function getListenersForEvent($event, array $identifiers = []) + { + yield from $this->getListeners($identifiers, $this->getEventName($event, __METHOD__)); + } + + /** + * {@inheritDoc} + */ + public function attach($identifier, $eventName, callable $listener, $priority = 1) + { + return $this->proxy->attach($identifier, $eventName, $listener, $priority); + } + + /** + * {@inheritDoc} + */ + public function detach(callable $listener, $identifier = null, $eventName = null) + { + return $this->proxy->detach($listener, $identifier, $eventName); + } + + /** + * {@inheritDoc} + */ + public function getListeners(array $identifiers, $eventName) + { + return $this->proxy->getListeners($identifiers, $this->getEventName($eventName)); + } + + /** + * {@inheritDoc} + */ + public function clearListeners($identifier, $eventName = null) + { + return $this->proxy->clearListeners($identifier, $eventName); + } + + /** + * @param mixed $event + * @param string $method Method that called this one + * @return string + */ + private function getEventName($event, $method) + { + if (is_string($event) && ! empty($event)) { + return $event; + } + + if (! is_object($event)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an object or non-empty string $event argument; received %s', + $method, + gettype($event) + )); + } + + if (is_callable([$event, 'getName'])) { + return $event->getName() ?: get_class($event); + } + + return get_class($event); + } +} diff --git a/src/Test/EventListenerIntrospectionTrait.php b/src/Test/EventListenerIntrospectionTrait.php index ed679cd..c5f7908 100644 --- a/src/Test/EventListenerIntrospectionTrait.php +++ b/src/Test/EventListenerIntrospectionTrait.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\Assert; use ReflectionProperty; +use Zend\EventManager\Event; use Zend\EventManager\EventManager; /** @@ -33,15 +34,20 @@ trait EventListenerIntrospectionTrait /** * Retrieve a list of event names from an event manager. * - * @param EventManager $events + * @param EventManager $manager * @return string[] */ - private function getEventsFromEventManager(EventManager $events) + private function getEventsFromEventManager(EventManager $manager) { - $r = new ReflectionProperty($events, 'events'); + $r = new ReflectionProperty($manager, 'prioritizedProvider'); + $r->setAccessible(true); + $provider = $r->getValue($manager); + + $r = new ReflectionProperty($provider, 'events'); $r->setAccessible(true); - $listeners = $r->getValue($events); - return array_keys($listeners); + $events = $r->getValue($provider); + + return array_keys($events); } /** @@ -64,18 +70,19 @@ private function getEventsFromEventManager(EventManager $events) */ private function getListenersForEvent($event, EventManager $events, $withPriority = false) { - $r = new ReflectionProperty($events, 'events'); - $r->setAccessible(true); - $internal = $r->getValue($events); + $event = new Event($event); - $listeners = []; - foreach (isset($internal[$event]) ? $internal[$event] : [] as $p => $listOfListeners) { - foreach ($listOfListeners as $l) { - $listeners[$p] = isset($listeners[$p]) ? array_merge($listeners[$p], $l) : $l; - } + if (! $withPriority) { + $listeners = $events->getListenersForEvent($event); + return iterator_to_array($listeners, false); } - return $this->traverseListeners($listeners, $withPriority); + $r = new ReflectionProperty($events, 'provider'); + $r->setAccessible(true); + $provider = $r->getValue($events); + + $listeners = $this->traverseListeners($provider->getListenersForEventByPriority($event), true); + return iterator_to_array($listeners); } /** @@ -125,7 +132,7 @@ private function assertListenerAtPriority( */ private function getArrayOfListenersForEvent($event, EventManager $events) { - return iterator_to_array($this->getListenersForEvent($event, $events)); + return $this->getListenersForEvent($event, $events); } /** diff --git a/test/EventManagerTest.php b/test/EventManagerTest.php index 96a5bf4..e4d4208 100644 --- a/test/EventManagerTest.php +++ b/test/EventManagerTest.php @@ -18,6 +18,7 @@ use Zend\EventManager\EventManager; use Zend\EventManager\Exception; use Zend\EventManager\ListenerAggregateInterface; +use Zend\EventManager\ListenerProvider; use Zend\EventManager\ResponseCollection; use Zend\EventManager\SharedEventManager; use Zend\EventManager\SharedEventManagerInterface; @@ -29,7 +30,7 @@ public function setUp() if (isset($this->message)) { unset($this->message); } - $this->events = new EventManager; + $this->events = new EventManager(); } /** @@ -54,16 +55,8 @@ public function getEventListFromManager(EventManager $manager) */ public function getListenersForEvent($event, EventManager $manager) { - $r = new ReflectionProperty($manager, 'events'); - $r->setAccessible(true); - $events = $r->getValue($manager); - - $listenersByPriority = isset($events[$event]) ? $events[$event] : []; - foreach ($listenersByPriority as $priority => & $listeners) { - $listeners = $listeners[0]; - } - - return $listenersByPriority; + $listeners = $manager->getListenersForEvent(new Event($event)); + return iterator_to_array($listeners, false); } public function testAttachShouldAddListenerToEvent() @@ -71,8 +64,6 @@ public function testAttachShouldAddListenerToEvent() $listener = [$this, __METHOD__]; $this->events->attach('test', $listener); $listeners = $this->getListenersForEvent('test', $this->events); - // Get first (and only) priority queue of listeners for event - $listeners = array_shift($listeners); $this->assertCount(1, $listeners); $this->assertContains($listener, $listeners); return [ @@ -99,15 +90,6 @@ public function testAttachShouldAddReturnTheListener($event) $this->assertSame($listener, $this->events->attach($event, $listener)); } - public function testAttachShouldAddEventIfItDoesNotExist() - { - $this->assertAttributeEmpty('events', $this->events); - $listener = $this->events->attach('test', [$this, __METHOD__]); - $events = $this->getEventListFromManager($this->events); - $this->assertNotEmpty($events); - $this->assertContains('test', $events); - } - public function testTriggerShouldTriggerAttachedListeners() { $listener = $this->events->attach('test', [$this, 'handleTestEvent']); @@ -456,7 +438,24 @@ public function testCanInjectSharedManagerDuringConstruction() { $shared = $this->prophesize(SharedEventManagerInterface::class)->reveal(); $events = new EventManager($shared); - $this->assertSame($shared, $events->getSharedManager()); + + $r = new ReflectionProperty($events, 'provider'); + $r->setAccessible(true); + $provider = $r->getValue($events); + + $this->assertInstanceOf(ListenerProvider\PrioritizedAggregateListenerProvider::class, $provider); + + $r = new ReflectionProperty($provider, 'default'); + $r->setAccessible(true); + $decorator = $r->getValue($provider); + + $this->assertInstanceOf(SharedEventManager\SharedEventManagerDecorator::class, $decorator); + + $r = new ReflectionProperty($decorator, 'proxy'); + $r->setAccessible(true); + $test = $r->getValue($decorator); + + $this->assertSame($shared, $test); } public function invalidEventsForAttach() @@ -495,8 +494,8 @@ public function testCanClearAllListenersForAnEvent() $this->events->attach($event, $listener); } - $this->assertEquals($events, $this->getEventListFromManager($this->events)); $this->events->clearListeners('foo'); + $this->assertCount( 0, $this->getListenersForEvent('foo', $this->events), @@ -572,8 +571,6 @@ public function testDetachDoesNothingIfEventIsNotPresentInManager() $this->events->attach('foo', $callback); $this->events->detach($callback, 'bar'); $listeners = $this->getListenersForEvent('foo', $this->events); - // get first (and only) priority queue from listeners - $listeners = array_shift($listeners); $this->assertContains($callback, $listeners); } @@ -604,8 +601,6 @@ public function testCanDetachWildcardListeners() // Next, verify it's not in any of the specific event queues foreach ($events as $event) { $listeners = $this->getListenersForEvent($event, $this->events); - // Get listeners for first and only priority queue - $listeners = array_shift($listeners); $this->assertCount(1, $listeners); $this->assertNotContains($wildcardListener, $listeners); } @@ -660,8 +655,6 @@ public function testCanDetachASingleListenerFromAnEventWithMultipleListeners() $this->events->attach('foo', $alternateListener); $listeners = $this->getListenersForEvent('foo', $this->events); - // Get the listeners for the first priority queue - $listeners = array_shift($listeners); $this->assertCount( 2, $listeners, @@ -677,8 +670,6 @@ public function testCanDetachASingleListenerFromAnEventWithMultipleListeners() $this->events->detach($listener, 'foo'); $listeners = $this->getListenersForEvent('foo', $this->events); - // Get the listeners for the first priority queue - $listeners = array_shift($listeners); $this->assertCount( 1, $listeners, @@ -722,7 +713,7 @@ public function testDetachRemovesAllOccurrencesOfListenerForEvent() } $listeners = $this->getListenersForEvent('foo', $this->events); - $this->assertCount(5, $listeners); + $this->assertCount(5, $listeners, var_export($listeners, true)); $this->events->detach($listener, 'foo'); @@ -731,38 +722,6 @@ public function testDetachRemovesAllOccurrencesOfListenerForEvent() $this->assertNotContains($listener, $listeners); } - public function eventsMissingNames() - { - $event = $this->prophesize(EventInterface::class); - $event->getName()->willReturn(''); - $callback = function ($result) { - }; - - // @codingStandardsIgnoreStart - // [ event, method to trigger, callback ] - return [ - 'trigger-empty-string' => ['', 'trigger', null], - 'trigger-until-empty-string' => ['', 'triggerUntil', $callback], - 'trigger-event-empty-name' => [$event->reveal(), 'triggerEvent', null], - 'trigger-event-until-empty-name' => [$event->reveal(), 'triggerEventUntil', $callback], - ]; - // @codingStandardsIgnoreEnd - } - - /** - * @dataProvider eventsMissingNames - */ - public function testTriggeringAnEventWithAnEmptyNameRaisesAnException($event, $method, $callback) - { - $this->expectException(Exception\RuntimeException::class); - $this->expectExceptionMessage('missing a name'); - if ($callback) { - $this->events->$method($callback, $event); - } else { - $this->events->$method($event); - } - } - public function testTriggerEventAcceptsEventInstanceAndTriggersListeners() { $event = $this->prophesize(EventInterface::class); diff --git a/test/EventManagerWithProviderTest.php b/test/EventManagerWithProviderTest.php new file mode 100644 index 0000000..938c4ea --- /dev/null +++ b/test/EventManagerWithProviderTest.php @@ -0,0 +1,114 @@ +prophesize(ListenerProviderInterface::class)->reveal(); + + $manager = EventManager::createUsingListenerProvider($provider); + + $this->assertInstanceOf(EventManager::class, $manager); + $this->assertAttributeSame($provider, 'provider', $manager); + $this->assertAttributeEmpty('prioritizedProvider', $manager); + + return $manager; + } + + public function testCanCreateInstanceWithPrioritizedListenerProvider() + { + $provider = $this->prophesize(ListenerProviderInterface::class); + $provider->willImplement(PrioritizedListenerAttachmentInterface::class); + + $manager = EventManager::createUsingListenerProvider($provider->reveal()); + + $this->assertInstanceOf(EventManager::class, $manager); + $this->assertAttributeSame($provider->reveal(), 'provider', $manager); + $this->assertAttributeSame($provider->reveal(), 'prioritizedProvider', $manager); + } + + public function attachableProviderMethods() + { + $listener = function ($e) { + }; + return [ + 'attach' => ['attach', ['foo', $listener, 100]], + 'attachWildcardListener' => ['attachWildcardListener', [$listener, 100]], + 'detach' => ['detach', [$listener, 'foo']], + 'detachWildcardListener' => ['detachWildcardListener', [$listener]], + 'clearListeners' => ['clearListeners', ['foo']], + ]; + } + + /** + * @dataProvider attachableProviderMethods + * @depends testCanCreateInstanceWithListenerProvider + * @param string $method Method to call on manager + * @param array $arguments Arguments to pass to $method + * @param EventManager $manager Event manager on which to call $method + */ + public function testAttachmentMethodsRaiseExceptionForNonAttachableProvider($method, array $arguments, EventManager $manager) + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('instance is not of type ' . PrioritizedListenerAttachmentInterface::class); + $manager->{$method}(...$arguments); + } + + /** + * @dataProvider attachableProviderMethods + * @depends testCanCreateInstanceWithPrioritizedListenerProvider + * @param string $method Method to call on manager + * @param array $arguments Arguments to pass to $method + */ + public function testAttachmentMethodsProxyToAttachableProvider($method, array $arguments) + { + // Creating instances here, because prophecies cannot be passed as dependencies + $provider = $this->prophesize(ListenerProviderInterface::class); + $provider->willImplement(PrioritizedListenerAttachmentInterface::class); + $manager = EventManager::createUsingListenerProvider($provider->reveal()); + + $manager->{$method}(...$arguments); + + $provider->{$method}(...$arguments)->shouldHaveBeenCalledTimes(1); + } + + public function testGetListenersForEventProxiesToProvider() + { + $event = (object) ['name' => 'test']; + $listener = function ($e) { + }; + + $listeners = [ + clone $listener, + clone $listener, + clone $listener, + ]; + + $provider = $this->prophesize(ListenerProviderInterface::class); + $provider + ->getListenersForEvent($event, []) + ->willReturn($listeners); + + $manager = EventManager::createUsingListenerProvider($provider->reveal()); + + $test = $manager->getListenersForEvent($event); + + $this->assertSame($listeners, iterator_to_array($test, false)); + } +} diff --git a/test/Test/EventListenerIntrospectionTraitTest.php b/test/Test/EventListenerIntrospectionTraitTest.php index 0629b86..47996bc 100644 --- a/test/Test/EventListenerIntrospectionTraitTest.php +++ b/test/Test/EventListenerIntrospectionTraitTest.php @@ -52,8 +52,6 @@ public function testGetListenersForEventReturnsIteratorOfListenersForEventInPrio $this->events->attach('foo', $callback2, 5); $listeners = $this->getListenersForEvent('foo', $this->events); - $this->assertInstanceOf(Traversable::class, $listeners); - $listeners = iterator_to_array($listeners); $this->assertEquals([ $callback5, @@ -81,8 +79,6 @@ public function testGetListenersForEventReturnsIteratorOfListenersInAttachmentOr $this->events->attach('foo', $callback2); $listeners = $this->getListenersForEvent('foo', $this->events); - $this->assertInstanceOf(Traversable::class, $listeners); - $listeners = iterator_to_array($listeners); $this->assertEquals([ $callback5, @@ -110,8 +106,6 @@ public function testGetListenersForEventCanReturnPriorityKeysWhenRequested() $this->events->attach('foo', $callback2, 5); $listeners = $this->getListenersForEvent('foo', $this->events, true); - $this->assertInstanceOf(Traversable::class, $listeners); - $listeners = iterator_to_array($listeners); $this->assertEquals([ 1 => $callback5, From cee468a1a662b2aee1f36067317e639699b57fa0 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 9 Apr 2019 16:48:16 -0500 Subject: [PATCH 19/26] feat: Adds EventDispatchingInterface Basically, a counterpart to the current EventManagerAwareInterface, but for EventDispatcherInterface composition. Also revises the Deprecations list, as we can keep EventManager as an EventDispatcherInterface implementation for 4.0. --- TODO-PSR-14.md | 10 ++-------- src/EventDispatchingInterface.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 src/EventDispatchingInterface.php diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 2ec4720..f24b4e4 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -88,15 +88,10 @@ - [x] it will return the event itself - [x] it will need to validate that it received an object before calling `triggerListeners` -- [ ] Additional utilities - - [ ] `EventDispatchingInterface` with a `getEventDispatcher()` method - - [ ] Alternate dispatcher implementation, `EventDispatcher` - - [ ] Should accept a listener provider interface to its constructor - - [ ] Should implement `EventDispatcherInterface` via duck-typing: it will - implement a `dispatch()` method only +- [x] Additional utilities + - [x] `EventDispatchingInterface` with a `getEventDispatcher()` method - [ ] Deprecations - [ ] `EventInterface` - - [ ] `EventManager` - [ ] `EventManagerInterface` - [ ] `EventManagerAwareInterface` - [ ] `EventManagerAwareTrait` @@ -117,7 +112,6 @@ - [ ] Removals - [ ] `EventInterface` - - [ ] `EventManager` - [ ] `EventManagerInterface` - [ ] `EventManagerAwareInterface` - [ ] `EventManagerAwareTrait` diff --git a/src/EventDispatchingInterface.php b/src/EventDispatchingInterface.php new file mode 100644 index 0000000..f832f8f --- /dev/null +++ b/src/EventDispatchingInterface.php @@ -0,0 +1,16 @@ + Date: Tue, 9 Apr 2019 17:06:00 -0500 Subject: [PATCH 20/26] feat: deprecate features that will be removed in version 4 - `EventInterface` - `EventManagerInterface` - `EventManagerAwareInterface` - `EventManagerAwareTrait` - `EventsCapableInterface` (points people to `EventDispatchingInterface`) - `SharedEventManager` - `SharedEventManagerInterface` - `SharedEventsCapableInterface` - `ListenerAggregateInterface` (points people to the `PrioritizedListenerAttachmentInterface`) - `ListenerAggregateTrait` (points people to `ListenerSubscriberTrait`) - `AbstractListenerAggregate` (points people to `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait`) - `ResponseCollection` (tells people to aggregate state/results in the event itself) - `LazyListener` (points people to `ListenerProvider\LazyListener`) - `LazyEventListener` (points people to `ListenerProvider\LazyListener`) - `LazyListenerAggregate` (points people to `ListenerProvider\LazyListenerSubscriber`) - `FilterChain` and `Filter` subnamespace (this should be done in a separate component) --- TODO-PSR-14.md | 32 ++++++++++++++-------------- src/AbstractListenerAggregate.php | 5 +++++ src/EventInterface.php | 3 +++ src/EventManagerAwareInterface.php | 2 ++ src/EventManagerAwareTrait.php | 1 + src/EventManagerInterface.php | 3 +++ src/EventsCapableInterface.php | 3 +++ src/Filter/FilterInterface.php | 3 +++ src/Filter/FilterIterator.php | 5 ++++- src/FilterChain.php | 3 +++ src/LazyEventListener.php | 3 +++ src/LazyListener.php | 3 +++ src/LazyListenerAggregate.php | 3 +++ src/ListenerAggregateInterface.php | 3 +++ src/ListenerAggregateTrait.php | 5 +++++ src/ResponseCollection.php | 5 +++++ src/SharedEventManagerInterface.php | 3 +++ src/SharedEventsCapableInterface.php | 2 ++ 18 files changed, 70 insertions(+), 17 deletions(-) diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index f24b4e4..19392f5 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -91,22 +91,22 @@ - [x] Additional utilities - [x] `EventDispatchingInterface` with a `getEventDispatcher()` method - [ ] Deprecations - - [ ] `EventInterface` - - [ ] `EventManagerInterface` - - [ ] `EventManagerAwareInterface` - - [ ] `EventManagerAwareTrait` - - [ ] `EventsCapableInterface` (point people to `EventDispatchingInterface`) - - [ ] `SharedEventManager` - - [ ] `SharedEventManagerInterface` - - [ ] `SharedEventsCapableInterface` - - [ ] `ListenerAggregateInterface` (point people to the `PrioritizedListenerAttachmentInterface`) - - [ ] `ListenerAggregateTrait` (point people to `ListenerSubscriberTrait`) - - [ ] `AbstractListenerAggregate` (point people to `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait`) - - [ ] `ResponseCollection` (tell people to aggregate state/results in the event itself) - - [ ] `LazyListener` (point people to `ListenerProvider\LazyListener`) - - [ ] `LazyEventListener` (point people to `ListenerProvider\LazyListener`) - - [ ] `LazyListenerAggregate` (point people to `ListenerProvider\LazyListenerSubscriber`) - - [ ] `FilterChain` and `Filter` subnamespace (this should be done in a separate component) + - [x] `EventInterface` + - [x] `EventManagerInterface` + - [x] `EventManagerAwareInterface` + - [x] `EventManagerAwareTrait` + - [x] `EventsCapableInterface` (point people to `EventDispatchingInterface`) + - [x] `SharedEventManager` + - [x] `SharedEventManagerInterface` + - [x] `SharedEventsCapableInterface` + - [x] `ListenerAggregateInterface` (point people to the `PrioritizedListenerAttachmentInterface`) + - [x] `ListenerAggregateTrait` (point people to `ListenerSubscriberTrait`) + - [x] `AbstractListenerAggregate` (point people to `AbstractListenerSubscriber` and/or `ListenerSubscriberTrait`) + - [x] `ResponseCollection` (tell people to aggregate state/results in the event itself) + - [x] `LazyListener` (point people to `ListenerProvider\LazyListener`) + - [x] `LazyEventListener` (point people to `ListenerProvider\LazyListener`) + - [x] `LazyListenerAggregate` (point people to `ListenerProvider\LazyListenerSubscriber`) + - [x] `FilterChain` and `Filter` subnamespace (this should be done in a separate component) ## 4.0.0 full release diff --git a/src/AbstractListenerAggregate.php b/src/AbstractListenerAggregate.php index 5cb0f80..e9fd2d0 100644 --- a/src/AbstractListenerAggregate.php +++ b/src/AbstractListenerAggregate.php @@ -11,6 +11,11 @@ /** * Abstract aggregate listener + * + * @deprecated since 3.3.0. This class will be removed in version 4.0.0, in + * favor of the ListenerProvider\AbstractListenerSubscriber. In most cases, + * subscribers should fully implement ListenerSubscriberInterface on their + * own, however. */ abstract class AbstractListenerAggregate implements ListenerAggregateInterface { diff --git a/src/EventInterface.php b/src/EventInterface.php index 11c3a5b..ccf8b8b 100644 --- a/src/EventInterface.php +++ b/src/EventInterface.php @@ -13,6 +13,9 @@ /** * Representation of an event + * + * @deprecated since 3.3.0. This interface will be removed in version 4.0, in + * favor of using simple PHP objects as events. */ interface EventInterface { diff --git a/src/EventManagerAwareInterface.php b/src/EventManagerAwareInterface.php index 42ccbcf..00a1b40 100644 --- a/src/EventManagerAwareInterface.php +++ b/src/EventManagerAwareInterface.php @@ -11,6 +11,8 @@ /** * Interface to automate setter injection for an EventManager instance + * + * @deprecated since 3.3.0. This interface will be removed in version 4.0. */ interface EventManagerAwareInterface extends EventsCapableInterface { diff --git a/src/EventManagerAwareTrait.php b/src/EventManagerAwareTrait.php index fff7cf8..ff2d631 100644 --- a/src/EventManagerAwareTrait.php +++ b/src/EventManagerAwareTrait.php @@ -20,6 +20,7 @@ * EventManager into your object when it is pulled from the ServiceManager. * * @see Zend\Mvc\Service\ServiceManagerConfig + * @deprecated since 3.3.0. This trait will be removed in version 4.0. */ trait EventManagerAwareTrait { diff --git a/src/EventManagerInterface.php b/src/EventManagerInterface.php index 49b1ec0..4beac78 100644 --- a/src/EventManagerInterface.php +++ b/src/EventManagerInterface.php @@ -11,6 +11,9 @@ /** * Interface for messengers + * + * @deprecated since 3.3.0; this interface will be removed in version 4.0, in + * favor of the PSR-14 EventDispatcherInterface and ListenerProviderInterface. */ interface EventManagerInterface extends SharedEventsCapableInterface { diff --git a/src/EventsCapableInterface.php b/src/EventsCapableInterface.php index 503ec97..636302d 100644 --- a/src/EventsCapableInterface.php +++ b/src/EventsCapableInterface.php @@ -11,6 +11,9 @@ /** * Interface indicating that an object composes an EventManagerInterface instance. + * + * @deprecated since 3.3.0. This interface will be removed in version 4.0, in + * favor of the EventDispatcherInterface introduced in version 3.3.0. */ interface EventsCapableInterface { diff --git a/src/Filter/FilterInterface.php b/src/Filter/FilterInterface.php index f8c239a..9d93fa2 100644 --- a/src/Filter/FilterInterface.php +++ b/src/Filter/FilterInterface.php @@ -13,6 +13,9 @@ /** * Interface for intercepting filter chains + * + * @deprecated since 3.3.0. This interface will be removed in version 4.0.0. No + * replacement is provided. */ interface FilterInterface { diff --git a/src/Filter/FilterIterator.php b/src/Filter/FilterIterator.php index e1cf173..a2f50da 100644 --- a/src/Filter/FilterIterator.php +++ b/src/Filter/FilterIterator.php @@ -16,7 +16,10 @@ * Specialized priority queue implementation for use with an intercepting * filter chain. * - * Allows removal + * Allows removal. + * + * @deprecated since 3.3.0. This class will be removed in version 4.0.0. No + * replacement is provided. */ class FilterIterator extends FastPriorityQueue { diff --git a/src/FilterChain.php b/src/FilterChain.php index 85e0423..49047c3 100644 --- a/src/FilterChain.php +++ b/src/FilterChain.php @@ -11,6 +11,9 @@ /** * FilterChain: intercepting filter manager + * + * @deprecated since 3.3.0. This class will be removed in version 4.0.0. No + * replacement is provided. */ class FilterChain implements Filter\FilterInterface { diff --git a/src/LazyEventListener.php b/src/LazyEventListener.php index be5cd7c..9f4de0f 100644 --- a/src/LazyEventListener.php +++ b/src/LazyEventListener.php @@ -22,6 +22,9 @@ * * - event: the event name to attach to. * - priority: the priority at which to attach the listener, if not the default. + * + * @deprecated since 3.3.0. This class will be removed in version 4.0.0, in + * favor of the ListenerProvider\LazyListener implementation. */ class LazyEventListener extends LazyListener { diff --git a/src/LazyListener.php b/src/LazyListener.php index b1e7d92..ef6a788 100644 --- a/src/LazyListener.php +++ b/src/LazyListener.php @@ -28,6 +28,9 @@ * * Pass instances directly to the event manager's `attach()` method as the * listener argument. + * + * @deprecated since 3.3.0. This class will be removed in version 4.0.0, in + * favor of the ListenerProvider\LazyListener implementation. */ class LazyListener { diff --git a/src/LazyListenerAggregate.php b/src/LazyListenerAggregate.php index 1462870..32f653a 100644 --- a/src/LazyListenerAggregate.php +++ b/src/LazyListenerAggregate.php @@ -26,6 +26,9 @@ * $container * )); * + * + * @deprecated since 3.3.0. This class will be removed in version 4.0.0 in + * favor of the ListenerProvider\LazyListenerSubscriber implementation. */ class LazyListenerAggregate implements ListenerAggregateInterface { diff --git a/src/ListenerAggregateInterface.php b/src/ListenerAggregateInterface.php index 910fbb5..40e352f 100644 --- a/src/ListenerAggregateInterface.php +++ b/src/ListenerAggregateInterface.php @@ -16,6 +16,9 @@ * with an EventManager, without an event name. The {@link attach()} method will * then be called with the current EventManager instance, allowing the class to * wire up one or more listeners. + * + * @deprecated since 3.3.0. This interface will be removed in version 4.0.0, in + * favor of the ListenerProvider\ListenerSubscriberInterface. */ interface ListenerAggregateInterface { diff --git a/src/ListenerAggregateTrait.php b/src/ListenerAggregateTrait.php index 7480422..99bc125 100644 --- a/src/ListenerAggregateTrait.php +++ b/src/ListenerAggregateTrait.php @@ -12,6 +12,11 @@ /** * Provides logic to easily create aggregate listeners, without worrying about * manually detaching events + * + * @deprecated since 3.3.0. This trait will be removed in version 4.0.0, in + * favor of the ListenerProvider\ListenerSubscriberTrait. In most cases, + * subscribers should fully implement ListenerSubscriberInterface on their + * own, however. */ trait ListenerAggregateTrait { diff --git a/src/ResponseCollection.php b/src/ResponseCollection.php index 57c5bd3..f4411f8 100644 --- a/src/ResponseCollection.php +++ b/src/ResponseCollection.php @@ -13,6 +13,11 @@ /** * Collection of signal handler return values + * + * @deprecated since 3.3.0. This class will be removed in version 4.0.0. + * Listeners should not return values, and any values that should be + * aggregated or used to stop propagation should be injected directly into + * the event instance itself, via its own published API. */ class ResponseCollection extends SplStack { diff --git a/src/SharedEventManagerInterface.php b/src/SharedEventManagerInterface.php index c245704..b9b1a28 100644 --- a/src/SharedEventManagerInterface.php +++ b/src/SharedEventManagerInterface.php @@ -11,6 +11,9 @@ /** * Interface for shared event listener collections + * + * @deprecated since 3.3.0. This interface will be removed in version 4.0; use + * listener providers instead. */ interface SharedEventManagerInterface { diff --git a/src/SharedEventsCapableInterface.php b/src/SharedEventsCapableInterface.php index f367acb..e2f080f 100644 --- a/src/SharedEventsCapableInterface.php +++ b/src/SharedEventsCapableInterface.php @@ -12,6 +12,8 @@ /** * Interface indicating that an object composes or can compose a * SharedEventManagerInterface instance. + * + * @deprecated since 3.3.0. This interface will be removed in version 4.0. */ interface SharedEventsCapableInterface { From 7dc24c8071c73ebf0dba904a794a5710e2b0d3aa Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 10 Apr 2019 09:26:55 -0500 Subject: [PATCH 21/26] feat: require container-interop ^1.2 We now require ^1.2 to ensure that PSR-11 interfaces are also present, allowing new classes to typehint only on the PSR-11 interfaces. This will allow compatibility to continue as the 1.2 variants extend the PSR-11 interfaces. --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2d39cf9..1ab62f2 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "phpbench/phpbench": "^0.13", "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", "zendframework/zend-stdlib": "^2.7.3 || ^3.0", - "container-interop/container-interop": "^1.1.0", + "container-interop/container-interop": "^1.2.0", "zendframework/zend-coding-standard": "~1.0.0" }, "suggest": { diff --git a/composer.lock b/composer.lock index c12fb5b..6d74179 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "071997918aabea3d2d5493df994ee3aa", + "content-hash": "9cb47a2e1dda7467ed4d3b6099b726e6", "packages": [], "packages-dev": [ { From 441e8dd4fbf55e3f623f299ff1b7664b035be045 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 10 Apr 2019 09:30:56 -0500 Subject: [PATCH 22/26] fix: Adapt assertion to work with PHP versions < 7.1 Previously, `assertInternalType('iterable')`, which only works on PHP 7.1+. --- .../PrioritizedIdentifierListenerProviderTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php b/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php index 6dc605a..b195ed3 100644 --- a/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php +++ b/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php @@ -7,9 +7,9 @@ namespace ZendTest\EventManager\ListenerProvider; -use ArrayIterator; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Traversable; use Zend\EventManager\Exception; use Zend\EventManager\ListenerProvider\PrioritizedIdentifierListenerProvider; @@ -133,7 +133,8 @@ public function testDetachDoesNothingIfIdentifierDoesNotContainEvent() public function testProviderReturnsEmptyListWhenNoListenersAttachedForEventAndIdentifier() { $test = $this->provider->getListenersForEvent('EVENT', ['IDENTIFIER']); - $this->assertInternalType('iterable', $test); + // instead of assertInternalType('iterable'), which requires PHP 7.1+: + $this->assertTrue(is_array($test) || $test instanceof Traversable); $this->assertCount(0, $test); } From df2b093fb5b5530b2d52016f874b3b8a9fdc196a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 10 Apr 2019 09:42:05 -0500 Subject: [PATCH 23/26] refactor: Make anonymous classes into actual test asset classes Required to allow us to test against PHP 5.6. --- .../AbstractListenerSubscriberTest.php | 20 +----------- test/ListenerProvider/LazyListenerTest.php | 18 +---------- .../ListenerSubscriberTraitTest.php | 19 +----------- .../TestAsset/CallbackSubscriber.php | 31 +++++++++++++++++++ .../TestAsset/ExtendedCallbackSubscriber.php | 28 +++++++++++++++++ .../TestAsset/MultipleListener.php | 26 ++++++++++++++++ 6 files changed, 88 insertions(+), 54 deletions(-) create mode 100644 test/ListenerProvider/TestAsset/CallbackSubscriber.php create mode 100644 test/ListenerProvider/TestAsset/ExtendedCallbackSubscriber.php create mode 100644 test/ListenerProvider/TestAsset/MultipleListener.php diff --git a/test/ListenerProvider/AbstractListenerSubscriberTest.php b/test/ListenerProvider/AbstractListenerSubscriberTest.php index e474e2d..46b9482 100644 --- a/test/ListenerProvider/AbstractListenerSubscriberTest.php +++ b/test/ListenerProvider/AbstractListenerSubscriberTest.php @@ -7,10 +7,6 @@ namespace ZendTest\EventManager\ListenerProvider; -use Closure; -use Zend\EventManager\ListenerProvider\AbstractListenerSubscriber; -use Zend\EventManager\ListenerProvider\PrioritizedListenerAttachmentInterface; - class AbstractListenerSubscriberTest extends ListenerSubscriberTraitTest { /** @@ -18,20 +14,6 @@ class AbstractListenerSubscriberTest extends ListenerSubscriberTraitTest */ public function createProvider(callable $attachmentCallback) { - return new class($attachmentCallback) extends AbstractListenerSubscriber { - /** @var callable */ - private $attachmentCallback; - - public function __construct(callable $attachmentCallback) - { - $this->attachmentCallback = $attachmentCallback; - } - - public function attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1) - { - $attachmentCallback = $this->attachmentCallback->bindTo($this, $this); - $attachmentCallback($provider, $priority); - } - }; + return new TestAsset\ExtendedCallbackSubscriber($attachmentCallback); } } diff --git a/test/ListenerProvider/LazyListenerTest.php b/test/ListenerProvider/LazyListenerTest.php index d68639b..17e11b6 100644 --- a/test/ListenerProvider/LazyListenerTest.php +++ b/test/ListenerProvider/LazyListenerTest.php @@ -10,7 +10,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Psr\Container\ContainerInterface; -use stdClass; use Zend\EventManager\EventInterface; use Zend\EventManager\Exception\InvalidArgumentException; use Zend\EventManager\ListenerProvider\LazyListener; @@ -134,22 +133,7 @@ public function methodsToInvoke() */ public function testInvocationInvokesMethodDefinedInListener($method, $expected) { - $listener = new class { - public function __invoke($e) - { - $e->value = __FUNCTION__; - } - - public function run($e) - { - $e->value = __FUNCTION__; - } - - public function onEvent($e) - { - $e->value = __FUNCTION__; - } - }; + $listener = new TestAsset\MultipleListener(); $this->container ->get('listener') diff --git a/test/ListenerProvider/ListenerSubscriberTraitTest.php b/test/ListenerProvider/ListenerSubscriberTraitTest.php index f98d4a1..2a1d938 100644 --- a/test/ListenerProvider/ListenerSubscriberTraitTest.php +++ b/test/ListenerProvider/ListenerSubscriberTraitTest.php @@ -10,7 +10,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Zend\EventManager\ListenerProvider\ListenerSubscriberInterface; -use Zend\EventManager\ListenerProvider\ListenerSubscriberTrait; use Zend\EventManager\ListenerProvider\PrioritizedListenerAttachmentInterface; class ListenerSubscriberTraitTest extends TestCase @@ -20,23 +19,7 @@ class ListenerSubscriberTraitTest extends TestCase */ public function createProvider(callable $attachmentCallback) { - return new class($attachmentCallback) implements ListenerSubscriberInterface { - use ListenerSubscriberTrait; - - /** @var callable */ - private $attachmentCallback; - - public function __construct(callable $attachmentCallback) - { - $this->attachmentCallback = $attachmentCallback; - } - - public function attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1) - { - $attachmentCallback = $this->attachmentCallback->bindTo($this, $this); - $attachmentCallback($provider, $priority); - } - }; + return new TestAsset\CallbackSubscriber($attachmentCallback); } public function testSubscriberAttachesListeners() diff --git a/test/ListenerProvider/TestAsset/CallbackSubscriber.php b/test/ListenerProvider/TestAsset/CallbackSubscriber.php new file mode 100644 index 0000000..ec4a904 --- /dev/null +++ b/test/ListenerProvider/TestAsset/CallbackSubscriber.php @@ -0,0 +1,31 @@ +attachmentCallback = $attachmentCallback; + } + + public function attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1) + { + $attachmentCallback = $this->attachmentCallback->bindTo($this, $this); + $attachmentCallback($provider, $priority); + } +} diff --git a/test/ListenerProvider/TestAsset/ExtendedCallbackSubscriber.php b/test/ListenerProvider/TestAsset/ExtendedCallbackSubscriber.php new file mode 100644 index 0000000..0d3bdaa --- /dev/null +++ b/test/ListenerProvider/TestAsset/ExtendedCallbackSubscriber.php @@ -0,0 +1,28 @@ +attachmentCallback = $attachmentCallback; + } + + public function attach(PrioritizedListenerAttachmentInterface $provider, $priority = 1) + { + $attachmentCallback = $this->attachmentCallback->bindTo($this, $this); + $attachmentCallback($provider, $priority); + } +} diff --git a/test/ListenerProvider/TestAsset/MultipleListener.php b/test/ListenerProvider/TestAsset/MultipleListener.php new file mode 100644 index 0000000..4334adb --- /dev/null +++ b/test/ListenerProvider/TestAsset/MultipleListener.php @@ -0,0 +1,26 @@ +value = __FUNCTION__; + } + + public function run($e) + { + $e->value = __FUNCTION__; + } + + public function onEvent($e) + { + $e->value = __FUNCTION__; + } +} From c3670374081d92f08172468ccfa54d4650fa1fe8 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 10 Apr 2019 09:45:45 -0500 Subject: [PATCH 24/26] qa: CS fixes per phpcs whitespace and long lines --- src/EventManager.php | 7 +++++-- test/EventManagerWithProviderTest.php | 7 +++++-- .../PrioritizedIdentifierListenerProviderTest.php | 14 ++++++++++++-- .../PrioritizedListenerProviderTest.php | 10 +++++----- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/EventManager.php b/src/EventManager.php index f6eb9ff..7274fb3 100644 --- a/src/EventManager.php +++ b/src/EventManager.php @@ -118,8 +118,11 @@ public static function createUsingListenerProvider( * createUsingListenerProvider to ensure that no provider is created during * instantiation. */ - public function __construct(SharedEventManagerInterface $sharedEventManager = null, array $identifiers = [], $skipProviderCreation = false) - { + public function __construct( + SharedEventManagerInterface $sharedEventManager = null, + array $identifiers = [], + $skipProviderCreation = false + ) { $this->eventPrototype = new Event(); if ($skipProviderCreation) { diff --git a/test/EventManagerWithProviderTest.php b/test/EventManagerWithProviderTest.php index 938c4ea..a46031a 100644 --- a/test/EventManagerWithProviderTest.php +++ b/test/EventManagerWithProviderTest.php @@ -63,8 +63,11 @@ public function attachableProviderMethods() * @param array $arguments Arguments to pass to $method * @param EventManager $manager Event manager on which to call $method */ - public function testAttachmentMethodsRaiseExceptionForNonAttachableProvider($method, array $arguments, EventManager $manager) - { + public function testAttachmentMethodsRaiseExceptionForNonAttachableProvider( + $method, + array $arguments, + EventManager $manager + ) { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('instance is not of type ' . PrioritizedListenerAttachmentInterface::class); $manager->{$method}(...$arguments); diff --git a/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php b/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php index b195ed3..b107711 100644 --- a/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php +++ b/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php @@ -22,8 +22,18 @@ public function setUp() $this->provider = new PrioritizedIdentifierListenerProvider(); } - public function getListeners(PrioritizedIdentifierListenerProvider $provider, array $identifiers, $event, $priority = 1) - { + /** + * @param string[] $identifiers + * @param string|object $event + * @param int $priority + * @return iterable + */ + public function getListeners( + PrioritizedIdentifierListenerProvider $provider, + array $identifiers, + $event, + $priority = 1 + ) { $priority = (int) $priority; $listeners = $provider->getListenersForEventByPriority($event, $identifiers); if (! isset($listeners[$priority])) { diff --git a/test/ListenerProvider/PrioritizedListenerProviderTest.php b/test/ListenerProvider/PrioritizedListenerProviderTest.php index f42961f..1964fdc 100644 --- a/test/ListenerProvider/PrioritizedListenerProviderTest.php +++ b/test/ListenerProvider/PrioritizedListenerProviderTest.php @@ -163,7 +163,7 @@ public function testCanDetachPreviouslyAttachedListenerFromEvent() $listener = function ($event) { }; $this->provider->attach('test', $listener); - + $event = $this->createEvent(); $listeners = iterator_to_array($this->provider->getListenersForEvent($event)); $this->assertSame([$listener], $listeners, 'Expected one listener for event; none found?'); @@ -179,7 +179,7 @@ public function testCanDetachListenerFromAllEventsUsingNullEventToDetach() }; $this->provider->attach('test', $listener); $this->provider->attach(Event::class, $listener); - + $event = $this->createEvent(); $listeners = $this->flattenListeners($this->provider->getListenersForEvent($event)); $this->assertSame([$listener, $listener], $listeners); @@ -195,7 +195,7 @@ public function testCanDetachListenerFromAllEventsViaDetachWildcardListener() }; $this->provider->attach('test', $listener); $this->provider->attach(Event::class, $listener); - + $event = $this->createEvent(); $listeners = $this->flattenListeners($this->provider->getListenersForEvent($event)); $this->assertSame([$listener, $listener], $listeners); @@ -212,7 +212,7 @@ public function testCanDetachWildcardListenerFromAllEvents() $this->provider->attachWildcardListener($listener); $this->provider->attach('test', $listener); $this->provider->attach(Event::class, $listener); - + $event = $this->createEvent(); $listeners = $this->flattenListeners($this->provider->getListenersForEvent($event)); $this->assertSame([$listener, $listener, $listener], $listeners); @@ -229,7 +229,7 @@ public function testCanClearListenersForASingleEventName() $this->provider->attachWildcardListener($listener); $this->provider->attach('test', $listener); $this->provider->attach(Event::class, $listener); - + $event = $this->createEvent(); $listeners = $this->flattenListeners($this->provider->getListenersForEvent($event)); $this->assertSame([$listener, $listener, $listener], $listeners); From af3635619d501a6bce82f3ded18463f73078d8c6 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 10 Apr 2019 10:30:44 -0500 Subject: [PATCH 25/26] docs: Updated CHANGELOG for #73 Notes all new features, major changes, and deprecations. --- CHANGELOG.md | 114 ++++++++++++++++++++++++++++++++++++++++++++++++- TODO-PSR-14.md | 2 +- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e0a88d..f003183 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,123 @@ All notable changes to this project will be documented in this file, in reverse - [#72](https://github.com/zendframework/zend-eventmanager/pull/72) adds support for PHP 7.3. +- [#73](https://github.com/zendframework/zend-eventmanager/pull/73) adds interfaces to allow duck-typing the EventManager as a [PSR-14](https://www.php-fig.org/psr/psr-14/) event + dispatcher. Full support is not provided yet as this version still supports + PHP 5.6. The new interfaces include: + + - `Zend\EventManager\EventDispatcherInterface` + - `Zend\EventManager\ListenerProvider\ListenerProviderInterface` + - `Zend\EventManager\StoppableEventInterface` + + These interfaces will be removed in version 4.0, in favor of the official + PSR-14 interfaces. + +- [#73](https://github.com/zendframework/zend-eventmanager/pull/73) adds the following interfaces: + - `Zend\EventManager\EventDispatchingInterface`, for indicating a class + composes an `EventDispatcherInterface` instance. This interface will replace + the `Zend\EventManager\EventsCapableInterface` in version 4.0. + - `Zend\Expressive\ListenerProvider\PrioritizedListenerProviderInterface`, + which extends the `ListenerProviderInterface`, and adds the method + `getListenersForEventByPriority($event, $identifiers = [])`. This method + will return a list of integer priority keys mapping to lists of callable + listeners. + - `Zend\Expressive\ListenerProvider\PrioritizedListenerAttachmentInterface`, + which provides methods for attaching and detaching listeners with optional + priority values. This interface largely replaces the various related methods + in the current `EventManagerInterface`, and is for use with listener + providers. + - `Zend\Expressive\ListenerProvider\ListenerSubscriberInterface`, for + indicating that a class can attach multiple listeners to a + `PrioritizedListenerAttachmentInterface` instance. This largely replaces the + current `ListenerAggregateInterface` functionality. Users should likely use + the PSR-14 utility package's `DelegatingProvider` instead, however. + +- [#73](https://github.com/zendframework/zend-eventmanager/pull/73) adds the following listener provider classes and utilities: + - `AbstractListenerSubscriber` and `ListenerSubscriberTrait` can be used to + provide a generic way to detach subscribers. In most cases, + `ListenerSubscriberInterface` implementations should define their own logic + for doing so. + - `PrioritizedListenerProvider` implements `PrioritizedListenerProviderInterface` + and `PrioritizedListenerAttachmentInterface` in order to provide the various + listener attachment and retrieval capabilities in previous versions of the + `EventManager` class. + - `PrioritizedIdentifierListenerProvider` implements `PrioritizedListenerProviderInterface` + and `SharedEventManagerInterface`, and provides all features of the + `SharedEventManager` class from previous versions of the package. + - `PrioritizedAggregateListenerProvider` implements `PrioritizedListenerProviderInterface` + and accepts a list of `PrioritizedListenerProviderInterface` instances and + optionally a generic `ListenerProviderInterface` instance to its + constructor. When retrieving listeners, it will loop through the + `PrioritizedListenerProviderInterface` instance in order, yielding from + each, and then, if present, yield from the generic + `ListenerProviderInterface` instance. This approach essentially replaces the + listener and shared listener aggregation in previous versions of the + `EventManager`. + - `LazyListener` combines the functionalities of `Zend\EventManager\LazyListener` + and `Zend\EventManager\LazyEventListener`. If no event or priority are + provided to the constructor, than the `getEvent()` and `getPriority()` + methods will each return `null`. When invoked, the listener will pull the + specified service from the provided DI container, and then invoke it. + - `LazyListenerSubscriber` implements `ListenerSubscriberInterface` and + accepts a list of `LazyListener` instances to its constructor; any + non-`LazyListener` instances or any that do not define an event will cause + th constructor to raise an exception. When its `attach()` method is called, + it attaches the lazy listeners based on the event an priority values it + pulls from them. + +- [#73](https://github.com/zendframework/zend-eventmanager/pull/73) adds the static method `createUsingListenerProvider()` to the `EventManager` + class. This method takes a `ListenerProviderInterface`, and will then pull + directly from it when triggering events. If the provider also implements + `PrioritizedListenerAttachmentInterface`, the various listener attachment + methods defined in `EventManager` will proxy to it. + +- [#73](https://github.com/zendframework/zend-eventmanager/pull/73) adds the static method `createUsingListenerProvider()` to the `EventManager` + ### Changed -- Nothing. +- [#73](https://github.com/zendframework/zend-eventmanager/pull/73) modifies the `SharedEventManager` class to extend the new + `Zend\EventManager\ListenerProvider\PrioritizedIdentifierListenerProvider` class. + +- [#73](https://github.com/zendframework/zend-eventmanager/pull/73) modifies the `EventManager` class as follows: + - It now implements each of `ListenerProviderInterface` and + `PrioritizedListenerAttachmentInterface`. + - If constructed normally, it will create a `PrioritizedListenerProvider` + instance, and use that for all listener attachment. If a + `SharedEventManagerInterface` is provided, it will create a + `PrioritizedAggregateListenerProvider` using its own + `PrioritizedListenerProvider` and the shared manager, and use that for + fetching listeners. + - Adds a `dispatch()` method as an alternative to the various `trigger*()` methods. ### Deprecated -- Nothing. +- [#73](https://github.com/zendframework/zend-eventmanager/pull/73) deprecates the following interfaces and classes: + - `Zend\EventManager\EventInterface`. Users should start using vanilla PHP + objects that encapsulate all expected behavior for setting and retrieving + values and otherwise mutating state, including how and when propagation of the + event should stop. + - `Zend\EventManager\EventManagerInterface`; start typehinting against the + PSR-14 `EventDispatcherInterface` (or, in the meantime, the package-specific + variant). + - `Zend\EventManager\EventManagerAwareInterface` + - `Zend\EventManager\EventManagerAwareTrait` + - `Zend\EventManager\EventsCapableInterface`; start using `EventDispatchingInterface` instead. + - `Zend\EventManager\SharedEventManager`; start using listener providers + instead, attaching to identifiers based on event types. + - `Zend\EventManager\SharedEventManagerInterface` + - `Zend\EventManager\SharedEventsCapableInterface` + - `Zend\EventManager\ListenerAggregateInterface`; use the new `ListenerSubscriberInterface` instead. + - `Zend\EventManager\ListenerAggregateTrait`; use the new + `ListenerSubscriberTrait`, or define your own detachment logic. + - `Zend\EventManager\AbstractListenerAggregate`; use the new + `AbstractListenerSubscriber`, or define your own detachment logic. + - `Zend\EventManager\ResponseCollection`; aggregate state in the event itself, + and have the event determine when propagation needs to stop. + - `Zend\EventManager\LazyListener`; use `Zend\EventManager\ListenerProvider\LazyListener` instead. + - `Zend\EventManager\LazyEventListener`; use `Zend\EventManager\ListenerProvider\LazyListener` instead. + - `Zend\EventManager\LazyListenerAggregate`; use `Zend\EventManager\ListenerProvider\LazyListenerSubscriber` instead. + - `Zend\EventManager\FilterChain` and the `Filter` subnamespace; these will + move to a separate package in the future. ### Removed diff --git a/TODO-PSR-14.md b/TODO-PSR-14.md index 19392f5..7685835 100644 --- a/TODO-PSR-14.md +++ b/TODO-PSR-14.md @@ -90,7 +90,7 @@ `triggerListeners` - [x] Additional utilities - [x] `EventDispatchingInterface` with a `getEventDispatcher()` method -- [ ] Deprecations +- [x] Deprecations - [x] `EventInterface` - [x] `EventManagerInterface` - [x] `EventManagerAwareInterface` From 357d508f4425b5c5d794a1a49901761b44b721a7 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 10 Apr 2019 10:49:33 -0500 Subject: [PATCH 26/26] fix: Adapt `yield from` statements to work in PHP 5.6 `yield from` was introduced in PHP 7. As such, to work in PHP 5.6, we need to modify the statements to iterate over the inner generator and yield results directly. --- src/EventManager.php | 6 ++++- .../PrioritizedAggregateListenerProvider.php | 23 ++++++++++++++----- .../PrioritizedIdentifierListenerProvider.php | 14 +++++++---- .../PrioritizedListenerProvider.php | 9 ++++++-- .../SharedEventManagerDecorator.php | 4 ++-- ...oritizedIdentifierListenerProviderTest.php | 2 +- 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/EventManager.php b/src/EventManager.php index 7274fb3..669ac8e 100644 --- a/src/EventManager.php +++ b/src/EventManager.php @@ -383,13 +383,17 @@ public function clearListeners($eventName) /** * {@inheritDoc} + * @todo Use `yield from` once we bump the minimum supported PHP version to 7+. * @deprecated This method will be removed in version 4.0, and EventManager * will no longer be its own listener provider; use external listener * providers and the createUsingListenerProvider method instead. */ public function getListenersForEvent($event) { - yield from $this->provider->getListenersForEvent($event, $this->identifiers); + // @todo Use `yield from $this->provider->getListenersForEvent(...) + foreach ($this->provider->getListenersForEvent($event, $this->identifiers) as $listener) { + yield $listener; + } } /** diff --git a/src/ListenerProvider/PrioritizedAggregateListenerProvider.php b/src/ListenerProvider/PrioritizedAggregateListenerProvider.php index c6b0b8d..42d0575 100644 --- a/src/ListenerProvider/PrioritizedAggregateListenerProvider.php +++ b/src/ListenerProvider/PrioritizedAggregateListenerProvider.php @@ -30,17 +30,24 @@ public function __construct(array $providers, ListenerProviderInterface $default /** * {@inheritDoc} + * @todo Use `yield from` once we bump the minimum supported PHP version to 7+. * @param string[] $identifiers Any identifiers to use when retrieving * listeners from child providers. */ public function getListenersForEvent($event, array $identifiers = []) { - yield from $this->iterateByPriority( - $this->getListenersForEventByPriority($event, $identifiers) - ); + // @todo `yield from $this->iterateByPriority(...)` + foreach ($this->iterateByPriority($this->getListenersForEventByPriority($event, $identifiers)) as $listener) { + yield $listener; + } + + if (! $this->default) { + return; + } - if ($this->default) { - yield from $this->default->getListenersForEvent($event, $identifiers); + // @todo `yield from $this->default->getListenersForEvent(...)` + foreach ($this->default->getListenersForEvent($event, $identifiers) as $listener) { + yield $listener; } } @@ -79,6 +86,7 @@ private function validateProviders(array $providers) } /** + * @todo Use `yield from` once we bump the minimum supported PHP version to 7+. * @param array $prioritizedListeners * @return iterable */ @@ -86,7 +94,10 @@ private function iterateByPriority($prioritizedListeners) { krsort($prioritizedListeners); foreach ($prioritizedListeners as $listeners) { - yield from $listeners; + // @todo `yield from $listeners` + foreach ($listeners as $listener) { + yield $listener; + } } } } diff --git a/src/ListenerProvider/PrioritizedIdentifierListenerProvider.php b/src/ListenerProvider/PrioritizedIdentifierListenerProvider.php index a37cb3b..0a42afe 100644 --- a/src/ListenerProvider/PrioritizedIdentifierListenerProvider.php +++ b/src/ListenerProvider/PrioritizedIdentifierListenerProvider.php @@ -30,15 +30,17 @@ class PrioritizedIdentifierListenerProvider implements /** * {@inheritDoc} + * @todo Use `yield from` once we bump the minimum supported PHP version to 7+. * @param array $identifiers Identifiers from which to match event listeners. * @throws Exception\InvalidArgumentException for invalid event types * @throws Exception\InvalidArgumentException for invalid identifier types */ public function getListenersForEvent($event, array $identifiers = []) { - yield from $this->iterateByPriority( - $this->getListenersForEventByPriority($event, $identifiers) - ); + // @todo `yield from $this->iterateByPriority(...)` + foreach ($this->iterateByPriority($this->getListenersForEventByPriority($event, $identifiers)) as $listener) { + yield $listener; + } } /** @@ -265,6 +267,7 @@ private function getEventList($event) } /** + * @todo Use `yield from` once we bump the minimum supported PHP version to 7+. * @param array $prioritizedListeners * @return iterable */ @@ -272,7 +275,10 @@ private function iterateByPriority($prioritizedListeners) { krsort($prioritizedListeners); foreach ($prioritizedListeners as $listeners) { - yield from $listeners; + // @todo `yield from $listeners` + foreach ($listeners as $listener) { + yield $listener; + } } } } diff --git a/src/ListenerProvider/PrioritizedListenerProvider.php b/src/ListenerProvider/PrioritizedListenerProvider.php index 4f8635d..c6e50be 100644 --- a/src/ListenerProvider/PrioritizedListenerProvider.php +++ b/src/ListenerProvider/PrioritizedListenerProvider.php @@ -41,7 +41,8 @@ class PrioritizedListenerProvider implements */ public function getListenersForEvent($event) { - yield from $this->iterateByPriority( + // @todo Use `yield from $this->iterateByPriority(...)` + return $this->iterateByPriority( $this->getListenersForEventByPriority($event) ); } @@ -188,6 +189,7 @@ public function clearListeners($event) } /** + * @todo Use `yield from` once we bump the minimum supported PHP version to 7+. * @param array $prioritizedListeners * @return iterable */ @@ -195,7 +197,10 @@ private function iterateByPriority($prioritizedListeners) { krsort($prioritizedListeners); foreach ($prioritizedListeners as $listeners) { - yield from $listeners; + // @todo `yield from $listeners` + foreach ($listeners as $listener) { + yield $listener; + } } } } diff --git a/src/SharedEventManager/SharedEventManagerDecorator.php b/src/SharedEventManager/SharedEventManagerDecorator.php index 32801cc..0d05b5e 100644 --- a/src/SharedEventManager/SharedEventManagerDecorator.php +++ b/src/SharedEventManager/SharedEventManagerDecorator.php @@ -27,12 +27,12 @@ public function __construct(SharedEventManagerInterface $proxy) /** * {@inheritDoc} - * @var array $identifiers Identifiers provided by dispatcher, if any. + * @var iterable $identifiers Identifiers provided by dispatcher, if any. * This argument is deprecated, and will be removed in version 4. */ public function getListenersForEvent($event, array $identifiers = []) { - yield from $this->getListeners($identifiers, $this->getEventName($event, __METHOD__)); + return $this->getListeners($identifiers, $this->getEventName($event, __METHOD__)); } /** diff --git a/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php b/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php index b107711..5a6da2e 100644 --- a/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php +++ b/test/ListenerProvider/PrioritizedIdentifierListenerProviderTest.php @@ -297,7 +297,7 @@ public function invalidEventNamesForFetchingListeners() { $types = $this->invalidEventNames(); unset($types['non-traversable-object']); - yield from $types; + return $types; } /**