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

Commit 650a6f6

Browse files
committed
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.
1 parent 2446145 commit 650a6f6

File tree

3 files changed

+454
-6
lines changed

3 files changed

+454
-6
lines changed

TODO-PSR-14.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525
- [x] `detachWildcardListener(callable $listener, $force = false)`
2626
(`detach($listener, '*', $force)` will proxy to this method)
2727
- [x] `clearListeners($event)`
28-
- [ ] Create a `PrioritizedListenerProvider` implementation of the above based
28+
- [x] Create a `PrioritizedListenerProvider` implementation of the above based
2929
on the internals of `EventManager`
30-
- [ ] attachment/detachment
31-
- [ ] getListenersForEvent should take into account event name if an EventInterface
32-
- [ ] getListenersForEvent should also pull wildcard listeners
33-
- [ ] getListenersForEvent should accept an optional second argument, an
30+
- [x] attachment/detachment
31+
- [x] getListenersForEvent should take into account event name if an EventInterface
32+
- [x] getListenersForEvent should also pull wildcard listeners
33+
- [x] getListenersForEvent should accept an optional second argument, an
3434
array of identifiers. This method will return all listeners in prioritized
3535
order.
36-
- [ ] implement `getListenersForEventByPriority`
36+
- [x] implement `getListenersForEventByPriority`
3737
- [ ] Create a `PrioritizedIdentifierListenerProvider` that implements
3838
both the `PrioritizedListenerProvider` interface and the
3939
`SharedEventManagerInterface`
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
12+
class PrioritizedListenerProvider implements
13+
PrioritizedListenerAttachmentInterface,
14+
PrioritizedListenerProviderInterface
15+
{
16+
/**
17+
* Subscribed events and their listeners
18+
*
19+
* STRUCTURE:
20+
* [
21+
* <string name> => [
22+
* <int priority> => [
23+
* 0 => [<callable listener>, ...]
24+
* ],
25+
* ...
26+
* ],
27+
* ...
28+
* ]
29+
*
30+
* NOTE:
31+
* This structure helps us to reuse the list of listeners
32+
* instead of first iterating over it and generating a new one
33+
* -> In result it improves performance by up to 25% even if it looks a bit strange
34+
*
35+
* @var array<string, array<int, callable[]>>
36+
*/
37+
protected $events = [];
38+
39+
/**
40+
* {@inheritDoc}
41+
*/
42+
public function getListenersForEvent($event)
43+
{
44+
yield from $this->iterateByPriority(
45+
$this->getListenersForEventByPriority($event)
46+
);
47+
}
48+
49+
/**
50+
* {@inheritDoc}
51+
* @param string[] $identifiers Ignored in this implementation.
52+
* @throws Exception\InvalidArgumentException for invalid $event types.
53+
*/
54+
public function getListenersForEventByPriority($event, array $identifiers = [])
55+
{
56+
if (! is_object($event)) {
57+
throw new Exception\InvalidArgumentException(sprintf(
58+
'%s expects the $event argument to be an object; received %s',
59+
__METHOD__,
60+
gettype($event)
61+
));
62+
}
63+
64+
$identifiers = is_callable([$event, 'getName'])
65+
? [$event->getName()]
66+
: [];
67+
$identifiers = array_merge($identifiers, [get_class($event), '*']);
68+
69+
$prioritizedListeners = [];
70+
foreach ($identifiers as $name) {
71+
if (! isset($this->events[$name])) {
72+
continue;
73+
}
74+
75+
foreach ($this->events[$name] as $priority => $listOfListeners) {
76+
$prioritizedListeners[$priority][] = $listOfListeners[0];
77+
}
78+
}
79+
80+
return $prioritizedListeners;
81+
}
82+
83+
/**
84+
* {@inheritDoc}
85+
* @throws Exception\InvalidArgumentException for invalid $event types.
86+
*/
87+
public function attach($event, callable $listener, $priority = 1)
88+
{
89+
if (! is_string($event)) {
90+
throw new Exception\InvalidArgumentException(sprintf(
91+
'%s expects a string for the event; received %s',
92+
__METHOD__,
93+
gettype($event)
94+
));
95+
}
96+
97+
$this->events[$event][(int) $priority][0][] = $listener;
98+
}
99+
100+
/**
101+
* {@inheritDoc}
102+
* @param bool $force Internal; used by attachWildcardListener to force
103+
* removal of the '*' event.
104+
* @throws Exception\InvalidArgumentException for invalid event types.
105+
*/
106+
public function detach(callable $listener, $event = null, $force = false)
107+
{
108+
if (null === $event || ('*' === $event && ! $force)) {
109+
$this->detachWildcardListener($listener);
110+
return;
111+
}
112+
113+
if (! is_string($event)) {
114+
throw new Exception\InvalidArgumentException(sprintf(
115+
'%s expects a string for the event; received %s',
116+
__METHOD__,
117+
gettype($event)
118+
));
119+
}
120+
121+
if (! isset($this->events[$event])) {
122+
return;
123+
}
124+
125+
foreach ($this->events[$event] as $priority => $listeners) {
126+
foreach ($listeners[0] as $index => $evaluatedListener) {
127+
if ($evaluatedListener !== $listener) {
128+
continue;
129+
}
130+
131+
// Found the listener; remove it.
132+
unset($this->events[$event][$priority][0][$index]);
133+
134+
// If the queue for the given priority is empty, remove it.
135+
if (empty($this->events[$event][$priority][0])) {
136+
unset($this->events[$event][$priority]);
137+
break;
138+
}
139+
}
140+
}
141+
142+
// If the queue for the given event is empty, remove it.
143+
if (empty($this->events[$event])) {
144+
unset($this->events[$event]);
145+
}
146+
}
147+
148+
/**
149+
* {@inheritDoc}
150+
*/
151+
public function attachWildcardListener(callable $listener, $priority = 1)
152+
{
153+
$this->events['*'][(int) $priority][0][] = $listener;
154+
}
155+
156+
/**
157+
* {@inheritDoc}
158+
*/
159+
public function detachWildcardListener(callable $listener)
160+
{
161+
foreach (array_keys($this->events) as $event) {
162+
$this->detach($listener, $event, true);
163+
}
164+
}
165+
166+
/**
167+
* {@inheritDoc}
168+
* @throws Exception\InvalidArgumentException for invalid event types.
169+
*/
170+
public function clearListeners($event)
171+
{
172+
if (! is_string($event)) {
173+
throw new Exception\InvalidArgumentException(sprintf(
174+
'%s expects a string for the event; received %s',
175+
__METHOD__,
176+
gettype($event)
177+
));
178+
}
179+
180+
if (isset($this->events[$event])) {
181+
unset($this->events[$event]);
182+
}
183+
}
184+
185+
/**
186+
* @param array $prioritizedListeners
187+
* @return iterable
188+
*/
189+
private function iterateByPriority($prioritizedListeners)
190+
{
191+
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;
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)