Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit 88b4ef6

Browse files
committed
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.
1 parent a16b4c0 commit 88b4ef6

File tree

4 files changed

+598
-18
lines changed

4 files changed

+598
-18
lines changed

TODO-PSR-14.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@
3434
array of identifiers. This method will return all listeners in prioritized
3535
order.
3636
- [x] implement `getListenersForEventByPriority`
37-
- [ ] Create a `PrioritizedIdentifierListenerProvider` that implements
37+
- [x] Create a `PrioritizedIdentifierListenerProvider` that implements
3838
both the `PrioritizedListenerProvider` interface and the
3939
`SharedEventManagerInterface`
40-
- [ ] implement `getListenersForEventByPriority`
41-
- [ ] `SharedEventManager` will extend this class
42-
- [ ] mark as deprecated (will not use this in v4)
40+
- [x] implement `getListenersForEventByPriority`
41+
- [x] `SharedEventManager` will extend this class
42+
- [x] mark as deprecated (will not use this in v4)
4343
- [ ] Create a `PrioritizedAggregateListenerProvider` implementation
4444
- [ ] Accepts a list of `PrioritizedListenerProvider` instances
4545
- [ ] `getListenersByEvent()` will loop through each, in order, calling the
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
<?php
2+
/**
3+
* @see https://github.com/zendframework/zend-eventmanager for the canonical source repository
4+
* @copyright Copyright (c) 2019 Zend Technologies USA Inc. (https://www.zend.com)
5+
* @license https://github.com/zendframework/zend-eventmanager/blob/master/LICENSE.md New BSD License
6+
*/
7+
8+
namespace Zend\EventManager\ListenerProvider;
9+
10+
use Zend\EventManager\Exception;
11+
use Zend\EventManager\SharedEventManagerInterface;
12+
13+
/**
14+
* @deprecated This implementation exists to allow the SharedEventManager
15+
* system to coexist with listener providers during the 3.X series of
16+
* releases, and will be removed in version 4. Use class and interface
17+
* names of events when attaching them to a provider instead, as those
18+
* are most equivalent to the identifier system.
19+
*/
20+
class PrioritizedIdentifierListenerProvider implements
21+
PrioritizedListenerProviderInterface,
22+
SharedEventManagerInterface
23+
{
24+
/**
25+
* Identifiers with event connections
26+
*
27+
* @var array<string, array<string, array<int, callable[]>>>
28+
*/
29+
protected $identifiers = [];
30+
31+
/**
32+
* {@inheritDoc}
33+
* @param array $identifiers Identifiers from which to match event listeners.
34+
* @throws Exception\InvalidArgumentException for invalid event types
35+
* @throws Exception\InvalidArgumentException for invalid identifier types
36+
*/
37+
public function getListenersForEvent($event, array $identifiers = [])
38+
{
39+
yield from $this->iterateByPriority(
40+
$this->getListenersForEventByPriority($event, $identifiers)
41+
);
42+
}
43+
44+
/**
45+
* {@inheritDoc}
46+
* @throws Exception\InvalidArgumentException for invalid event types
47+
* @throws Exception\InvalidArgumentException for invalid identifier types
48+
*/
49+
public function getListenersForEventByPriority($event, array $identifiers = [])
50+
{
51+
$this->validateEventForListenerRetrieval($event, __METHOD__);
52+
53+
$prioritizedListeners = [];
54+
$identifiers = $this->normalizeIdentifierList($identifiers);
55+
$eventList = $this->getEventList($event);
56+
57+
foreach ($identifiers as $identifier) {
58+
if (! is_string($identifier) || empty($identifier)) {
59+
throw new Exception\InvalidArgumentException(sprintf(
60+
'Identifier names passed to %s must be non-empty',
61+
__METHOD__
62+
));
63+
}
64+
65+
if (! isset($this->identifiers[$identifier])) {
66+
continue;
67+
}
68+
69+
$listenersByIdentifier = $this->identifiers[$identifier];
70+
71+
foreach ($eventList as $eventName) {
72+
if (! isset($listenersByIdentifier[$eventName])) {
73+
continue;
74+
}
75+
76+
foreach ($listenersByIdentifier[$eventName] as $priority => $listOfListeners) {
77+
$prioritizedListeners[$priority] = isset($prioritizedListeners[$priority])
78+
? array_merge($prioritizedListeners[$priority], $listOfListeners[0])
79+
: $listOfListeners[0];
80+
}
81+
}
82+
}
83+
84+
return $prioritizedListeners;
85+
}
86+
87+
/**
88+
* {@inheritDoc}
89+
* @throws Exception\InvalidArgumentException for invalid identifier types
90+
* @throws Exception\InvalidArgumentException for invalid event types
91+
*/
92+
public function attach($identifier, $eventName, callable $listener, $priority = 1)
93+
{
94+
if (! is_string($identifier) || empty($identifier)) {
95+
throw new Exception\InvalidArgumentException(sprintf(
96+
'Invalid identifier provided; must be a string; received "%s"',
97+
gettype($identifier)
98+
));
99+
}
100+
101+
if (! is_string($eventName) || empty($eventName)) {
102+
throw new Exception\InvalidArgumentException(sprintf(
103+
'Invalid event provided; must be a non-empty string; received "%s"',
104+
gettype($eventName)
105+
));
106+
}
107+
108+
$this->identifiers[$identifier][$eventName][(int) $priority][0][] = $listener;
109+
}
110+
111+
/**
112+
* {@inheritDoc}
113+
* @param bool $force Internal; allows recursing when detaching wildcard listeners
114+
* @throws Exception\InvalidArgumentException for invalid identifier types
115+
* @throws Exception\InvalidArgumentException for invalid event name types
116+
*/
117+
public function detach(callable $listener, $identifier = null, $eventName = null, $force = false)
118+
{
119+
// No identifier or wildcard identifier: loop through all identifiers and detach
120+
if (null === $identifier || ('*' === $identifier && ! $force)) {
121+
foreach (array_keys($this->identifiers) as $identifier) {
122+
$this->detach($listener, $identifier, $eventName, true);
123+
}
124+
return;
125+
}
126+
127+
if (! is_string($identifier) || empty($identifier)) {
128+
throw new Exception\InvalidArgumentException(sprintf(
129+
'Invalid identifier provided; must be a string, received %s',
130+
gettype($identifier)
131+
));
132+
}
133+
134+
// Do we have any listeners on the provided identifier?
135+
if (! isset($this->identifiers[$identifier])) {
136+
return;
137+
}
138+
139+
if (null === $eventName || ('*' === $eventName && ! $force)) {
140+
foreach (array_keys($this->identifiers[$identifier]) as $eventName) {
141+
$this->detach($listener, $identifier, $eventName, true);
142+
}
143+
return;
144+
}
145+
146+
if (! is_string($eventName) || empty($eventName)) {
147+
throw new Exception\InvalidArgumentException(sprintf(
148+
'Invalid event name provided; must be a string, received %s',
149+
gettype($eventName)
150+
));
151+
}
152+
153+
if (! isset($this->identifiers[$identifier][$eventName])) {
154+
return;
155+
}
156+
157+
foreach ($this->identifiers[$identifier][$eventName] as $priority => $listOfListeners) {
158+
foreach ($listOfListeners[0] as $index => $evaluatedListener) {
159+
if ($evaluatedListener !== $listener) {
160+
continue;
161+
}
162+
163+
// Found the listener; remove it.
164+
unset($this->identifiers[$identifier][$eventName][$priority][0][$index]);
165+
166+
// Is the priority queue empty?
167+
if (empty($this->identifiers[$identifier][$eventName][$priority][0])) {
168+
unset($this->identifiers[$identifier][$eventName][$priority]);
169+
break;
170+
}
171+
}
172+
173+
// Is the event queue empty?
174+
if (empty($this->identifiers[$identifier][$eventName])) {
175+
unset($this->identifiers[$identifier][$eventName]);
176+
break;
177+
}
178+
}
179+
180+
// Is the identifier queue now empty? Remove it.
181+
if (empty($this->identifiers[$identifier])) {
182+
unset($this->identifiers[$identifier]);
183+
}
184+
}
185+
186+
/**
187+
* {@inheritDoc}
188+
*/
189+
public function getListeners(array $identifiers, $eventName)
190+
{
191+
return $this->getListenersForEventByPriority($eventName, $identifiers);
192+
}
193+
194+
/**
195+
* {@inheritDoc}
196+
*/
197+
public function clearListeners($identifier, $eventName = null)
198+
{
199+
if (! isset($this->identifiers[$identifier])) {
200+
return false;
201+
}
202+
203+
if (null === $eventName) {
204+
unset($this->identifiers[$identifier]);
205+
return;
206+
}
207+
208+
if (! isset($this->identifiers[$identifier][$eventName])) {
209+
return;
210+
}
211+
212+
unset($this->identifiers[$identifier][$eventName]);
213+
}
214+
215+
/**
216+
* @param mixed $event Event to validate
217+
* @param string $method Method name invoking this one
218+
* @return void
219+
* @throws Exception\InvalidArgumentException for invalid event types
220+
*/
221+
private function validateEventForListenerRetrieval($event, $method)
222+
{
223+
if (is_object($event)) {
224+
return;
225+
}
226+
227+
if (is_string($event) && '*' !== $event && ! empty($event)) {
228+
return;
229+
}
230+
231+
throw new Exception\InvalidArgumentException(sprintf(
232+
'Event name passed to %s must be a non-empty, non-wildcard string or an object',
233+
$method
234+
));
235+
}
236+
237+
/**
238+
* Deduplicate identifiers, and ensure wildcard identifier is last.
239+
*
240+
* @return string[]
241+
*/
242+
private function normalizeIdentifierList(array $identifiers)
243+
{
244+
$identifiers = array_unique($identifiers);
245+
if (false !== ($index = array_search('*', $identifiers, true))) {
246+
unset($identifiers[$index]);
247+
}
248+
array_push($identifiers, '*');
249+
return $identifiers;
250+
}
251+
252+
/**
253+
* @param string|object $event
254+
* @return string[]
255+
*/
256+
private function getEventList($event)
257+
{
258+
if (is_string($event)) {
259+
return [$event, '*'];
260+
}
261+
262+
return is_callable([$event, 'getName'])
263+
? [$event->getName(), get_class($event), '*']
264+
: [get_class($event), '*'];
265+
}
266+
267+
/**
268+
* @param array $prioritizedListeners
269+
* @return iterable
270+
*/
271+
private function iterateByPriority($prioritizedListeners)
272+
{
273+
krsort($prioritizedListeners);
274+
foreach ($prioritizedListeners as $listeners) {
275+
yield from $listeners;
276+
}
277+
}
278+
}

src/ListenerProvider/PrioritizedListenerProvider.php

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ public function getListenersForEventByPriority($event, array $identifiers = [])
7373
}
7474

7575
foreach ($this->events[$name] as $priority => $listOfListeners) {
76-
$prioritizedListeners[$priority][] = $listOfListeners[0];
76+
$prioritizedListeners[$priority] = isset($prioritizedListeners[$priority])
77+
? array_merge($prioritizedListeners[$priority], $listOfListeners[0])
78+
: $listOfListeners[0];
7779
}
7880
}
7981

@@ -189,19 +191,8 @@ public function clearListeners($event)
189191
private function iterateByPriority($prioritizedListeners)
190192
{
191193
krsort($prioritizedListeners);
192-
foreach ($prioritizedListeners as $listenerSets) {
193-
yield from $this->iterateListenerSets($listenerSets);
194-
}
195-
}
196-
197-
/**
198-
* @param iterable $listenerSets
199-
* @return iterable
200-
*/
201-
private function iterateListenerSets($listenerSets)
202-
{
203-
foreach ($listenerSets as $listOfListeners) {
204-
yield from $listOfListeners;
194+
foreach ($prioritizedListeners as $listeners) {
195+
yield from $listeners;
205196
}
206197
}
207198
}

0 commit comments

Comments
 (0)