1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Yiisoft\EventDispatcher\Provider; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Listener collection stores listeners and is used to configure provider. |
9
|
|
|
* |
10
|
|
|
* @see Provider |
11
|
|
|
*/ |
12
|
|
|
final class ListenerCollection |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* @var callable[] |
16
|
|
|
*/ |
17
|
|
|
private array $listeners = []; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @param string ...$eventClassNames |
21
|
|
|
* @return iterable<callable> |
22
|
|
|
*/ |
23
|
7 |
|
public function getForEvents(string ...$eventClassNames): iterable |
24
|
|
|
{ |
25
|
7 |
|
foreach ($eventClassNames as $eventClassName) { |
26
|
7 |
|
if (isset($this->listeners[$eventClassName])) { |
27
|
7 |
|
yield from $this->listeners[$eventClassName]; |
28
|
|
|
} |
29
|
|
|
} |
30
|
7 |
|
} |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Attaches listener to corresponding event based on the type-hint used for the event argument. |
34
|
|
|
* |
35
|
|
|
* Method signature should be the following: |
36
|
|
|
* |
37
|
|
|
* ``` |
38
|
|
|
* function (MyEvent $event): void |
39
|
|
|
* ``` |
40
|
|
|
* |
41
|
|
|
* Any callable could be used be it a closure, invokable object or array referencing a class or object. |
42
|
|
|
* |
43
|
|
|
* @param callable $listener |
44
|
|
|
* @param string $eventClassName |
45
|
|
|
* @return self |
46
|
|
|
*/ |
47
|
8 |
|
public function add(callable $listener, string $eventClassName = ''): self |
48
|
|
|
{ |
49
|
8 |
|
$new = clone $this; |
50
|
|
|
|
51
|
8 |
|
if ($eventClassName === '') { |
52
|
7 |
|
$eventClassName = $this->getParameterType($listener); |
53
|
|
|
} |
54
|
|
|
|
55
|
7 |
|
$new->listeners[$eventClassName][] = $listener; |
56
|
7 |
|
return $new; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Derives the interface type of the first argument of a callable. |
61
|
|
|
* |
62
|
|
|
* @suppress PhanUndeclaredMethod |
63
|
|
|
* |
64
|
|
|
* @param callable $callable The callable for which we want the parameter type. |
65
|
|
|
* @return string The interface the parameter is type hinted on. |
66
|
|
|
*/ |
67
|
7 |
|
private function getParameterType(callable $callable): string |
68
|
|
|
{ |
69
|
|
|
// This try-catch is only here to keep OCD linters happy about uncaught reflection exceptions. |
70
|
|
|
try { |
71
|
|
|
switch (true) { |
72
|
|
|
// See note on isClassCallable() for why this must be the first case. |
73
|
7 |
|
case $this->isClassCallable($callable): |
|
|
|
|
74
|
1 |
|
$reflect = new \ReflectionClass($callable[0]); |
75
|
1 |
|
$params = $reflect->getMethod($callable[1])->getParameters(); |
76
|
1 |
|
break; |
77
|
6 |
|
case $this->isFunctionCallable($callable): |
78
|
5 |
|
case $this->isClosureCallable($callable): |
79
|
4 |
|
$reflect = new \ReflectionFunction($callable); |
80
|
4 |
|
$params = $reflect->getParameters(); |
81
|
4 |
|
break; |
82
|
2 |
|
case $this->isObjectCallable($callable): |
83
|
1 |
|
$reflect = new \ReflectionObject($callable[0]); |
84
|
1 |
|
$params = $reflect->getMethod($callable[1])->getParameters(); |
85
|
1 |
|
break; |
86
|
1 |
|
case $this->isInvokable($callable): |
87
|
1 |
|
$params = (new \ReflectionMethod($callable, '__invoke'))->getParameters(); |
88
|
1 |
|
break; |
89
|
|
|
default: |
90
|
|
|
throw new \InvalidArgumentException('Not a recognized type of callable'); |
91
|
|
|
} |
92
|
|
|
|
93
|
7 |
|
$reflectedType = isset($params[0]) ? $params[0]->getType() : null; |
94
|
7 |
|
if ($reflectedType === null) { |
95
|
1 |
|
throw new \InvalidArgumentException('Listeners must declare an object type they can accept.'); |
96
|
|
|
} |
97
|
6 |
|
$type = $reflectedType->getName(); |
98
|
1 |
|
} catch (\ReflectionException $e) { |
99
|
|
|
throw new \RuntimeException('Type error registering listener.', 0, $e); |
100
|
|
|
} |
101
|
|
|
|
102
|
6 |
|
return $type; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Determines if a callable represents a function. |
107
|
|
|
* |
108
|
|
|
* Or at least a reasonable approximation, since a function name may not be defined yet. |
109
|
|
|
* |
110
|
|
|
* @param callable $callable |
111
|
|
|
* @return True if the callable represents a function, false otherwise. |
112
|
|
|
*/ |
113
|
6 |
|
private function isFunctionCallable(callable $callable): bool |
114
|
|
|
{ |
115
|
|
|
// We can't check for function_exists() because it may be included later by the time it matters. |
116
|
6 |
|
return is_string($callable); |
|
|
|
|
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Determines if a callable represents a closure/anonymous function. |
121
|
|
|
* |
122
|
|
|
* @param callable $callable |
123
|
|
|
* @return True if the callable represents a closure object, false otherwise. |
124
|
|
|
*/ |
125
|
5 |
|
private function isClosureCallable(callable $callable): bool |
126
|
|
|
{ |
127
|
5 |
|
return $callable instanceof \Closure; |
|
|
|
|
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* @param callable $callable |
132
|
|
|
* @return True if the callable represents an invokable object, false otherwise. |
133
|
|
|
*/ |
134
|
1 |
|
private function isInvokable(callable $callable): bool |
135
|
|
|
{ |
136
|
1 |
|
return is_object($callable); |
|
|
|
|
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Determines if a callable represents a method on an object. |
141
|
|
|
* |
142
|
|
|
* @param callable $callable |
143
|
|
|
* @return True if the callable represents a method object, false otherwise. |
144
|
|
|
*/ |
145
|
2 |
|
private function isObjectCallable(callable $callable): bool |
146
|
|
|
{ |
147
|
2 |
|
return is_array($callable) && is_object($callable[0]); |
|
|
|
|
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Determines if a callable represents a static class method. |
152
|
|
|
* |
153
|
|
|
* The parameter here is untyped so that this method may be called with an |
154
|
|
|
* array that represents a class name and a non-static method. The routine |
155
|
|
|
* to determine the parameter type is identical to a static method, but such |
156
|
|
|
* an array is still not technically callable. Omitting the parameter type here |
157
|
|
|
* allows us to use this method to handle both cases. |
158
|
|
|
* |
159
|
|
|
* Note that this method must therefore be the first in the switch statement |
160
|
|
|
* above, or else subsequent calls will break as the array is not going to satisfy |
161
|
|
|
* the callable type hint but it would pass `is_callable()`. Because PHP. |
162
|
|
|
* |
163
|
|
|
* @param callable $callable |
164
|
|
|
* @return True if the callable represents a static method, false otherwise. |
165
|
|
|
*/ |
166
|
7 |
|
private function isClassCallable($callable): bool |
167
|
|
|
{ |
168
|
7 |
|
return is_array($callable) && is_string($callable[0]) && class_exists($callable[0]); |
|
|
|
|
169
|
|
|
} |
170
|
|
|
} |
171
|
|
|
|
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.