Total Complexity | 44 |
Total Lines | 361 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like ListenerProviderPass often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ListenerProviderPass, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
41 | final class ListenerProviderPass implements CompilerPassInterface |
||
42 | { |
||
43 | public const DEFAULT_PROVIDER_TAG = 'listener_provider'; |
||
44 | public const DEFAULT_LISTENER_TAG = 'event_listener'; |
||
45 | public const DEFAULT_PRIORITY = 0; |
||
46 | public const ATTRIBUTE_LISTENER_TAG = 'listener_tag'; |
||
47 | public const ATTRIBUTE_EVENT = 'event'; |
||
48 | public const ATTRIBUTE_PRIORITY = 'priority'; |
||
49 | public const ATTRIBUTE_BEFORE = 'before'; |
||
50 | public const ATTRIBUTE_AFTER = 'after'; |
||
51 | public const PRIORITY_FIRST = 'first'; |
||
52 | public const PRIORITY_LAST = 'last'; |
||
53 | |||
54 | private const PLACEMENT_ATTRIBUTES = [ |
||
55 | self::ATTRIBUTE_PRIORITY, |
||
56 | self::ATTRIBUTE_BEFORE, |
||
57 | self::ATTRIBUTE_AFTER, |
||
58 | ]; |
||
59 | |||
60 | /** |
||
61 | * @var string |
||
62 | */ |
||
63 | private $providerTag; |
||
64 | |||
65 | /** |
||
66 | * @param string $providerTag Tag identifying listener providers. |
||
67 | */ |
||
68 | public function __construct(string $providerTag = self::DEFAULT_PROVIDER_TAG) |
||
69 | { |
||
70 | $this->providerTag = $providerTag; |
||
71 | } |
||
72 | |||
73 | /** |
||
74 | * @inheritDoc |
||
75 | */ |
||
76 | public function process(ContainerBuilder $container): void |
||
77 | { |
||
78 | foreach ($this->providerIterator($container) as $id => $listenerTag) { |
||
79 | [ $mapping, $refMap ] = $this->collectListeners($container, $listenerTag); |
||
80 | |||
81 | $container |
||
82 | ->getDefinition($id) |
||
83 | ->setSynthetic(false) |
||
84 | ->setClass(ListenerProviderWithContainer::class) |
||
85 | ->setArguments( |
||
86 | [ |
||
87 | $mapping, |
||
88 | ServiceLocatorTagPass::register($container, $refMap), |
||
89 | ] |
||
90 | ); |
||
91 | } |
||
92 | } |
||
93 | |||
94 | /** |
||
95 | * @return iterable<string, string> |
||
96 | */ |
||
97 | private function providerIterator(ContainerBuilder $container): iterable |
||
98 | { |
||
99 | foreach ($container->findTaggedServiceIds($this->providerTag, true) as $id => $tags) { |
||
100 | $listener_tag = $tags[0][self::ATTRIBUTE_LISTENER_TAG] ?? self::DEFAULT_LISTENER_TAG; |
||
101 | |||
102 | yield $id => $listener_tag; |
||
103 | } |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * @return array{0: array<class-string, string[]>, 1: array<string, TypedReference>} |
||
108 | */ |
||
109 | private function collectListeners(ContainerBuilder $container, string $listenerTag): array |
||
110 | { |
||
111 | $listeners = $container->findTaggedServiceIds($listenerTag, true); |
||
112 | $mapping = []; |
||
113 | $refMap = []; |
||
114 | $prioritiesByEvent = []; |
||
115 | $beforeByEvent = []; |
||
116 | $afterByEvent = []; |
||
117 | |||
118 | foreach ($listeners as $id => $tags) { |
||
119 | foreach ($tags as $attributes) { |
||
120 | $class = $container->getDefinition($id)->getClass(); |
||
121 | |||
122 | if (!$class) { |
||
123 | throw new InvalidArgumentException("Missing class for listener '$id'."); |
||
124 | } |
||
125 | |||
126 | $refMap[$id] = new TypedReference($id, $class); |
||
127 | |||
128 | $event = $this->extractEvent($attributes, $id); |
||
129 | $mapping[$event][] = $id; |
||
130 | |||
131 | $this->assertPlacement($attributes, $id); |
||
132 | |||
133 | if (isset($attributes[self::ATTRIBUTE_BEFORE])) { |
||
134 | $relative = $attributes[self::ATTRIBUTE_BEFORE]; |
||
135 | $this->assertRelative($listeners, $relative, $id); |
||
136 | $beforeByEvent[$event][$id] = $relative; |
||
137 | } elseif (isset($attributes[self::ATTRIBUTE_AFTER])) { |
||
138 | $relative = $attributes[self::ATTRIBUTE_AFTER]; |
||
139 | $this->assertRelative($listeners, $relative, $id); |
||
140 | $afterByEvent[$event][$id] = $relative; |
||
141 | } else { |
||
142 | $prioritiesByEvent[$event][$id] = $this->extractPriority($attributes, $id); |
||
143 | } |
||
144 | } |
||
145 | } |
||
146 | |||
147 | return [ |
||
148 | $this->sortMapping($mapping, $prioritiesByEvent, $beforeByEvent, $afterByEvent), |
||
149 | $refMap, |
||
150 | ]; |
||
151 | } |
||
152 | |||
153 | /** |
||
154 | * @param array<string, mixed> $tag |
||
155 | * |
||
156 | * @return class-string |
||
157 | */ |
||
158 | private function extractEvent(array $tag, string $id): string |
||
159 | { |
||
160 | $event = $tag[self::ATTRIBUTE_EVENT] ?? null; |
||
161 | |||
162 | if (!$event) { |
||
163 | $attribute = self::ATTRIBUTE_EVENT; |
||
164 | |||
165 | throw new InvalidArgumentException( |
||
166 | "Missing event type for listener '$id'." |
||
167 | . " Try to specify the event using the attribute '$attribute'." |
||
168 | ); |
||
169 | } |
||
170 | |||
171 | if (!class_exists($event) && !interface_exists($event)) { |
||
172 | throw new InvalidArgumentException( |
||
173 | "Unable to load event class or interface '$event' for listener '$id'." |
||
174 | ); |
||
175 | } |
||
176 | |||
177 | return $event; |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * @param array<string, mixed> $attributes |
||
182 | */ |
||
183 | public function assertPlacement(array $attributes, string $id): void |
||
184 | { |
||
185 | $positions = array_intersect(self::PLACEMENT_ATTRIBUTES, array_keys($attributes)); |
||
186 | |||
187 | if (count($positions) > 1) { |
||
188 | throw new LogicException(sprintf( |
||
189 | "Invalid definition for listener '$id', can only specify one of: %s.", |
||
190 | implode(', ', self::PLACEMENT_ATTRIBUTES) |
||
191 | )); |
||
192 | } |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * @param array<string, mixed> $listeners |
||
197 | */ |
||
198 | public function assertRelative(array $listeners, string $relative, string $id): void |
||
199 | { |
||
200 | if (!isset($listeners[$relative])) { |
||
201 | throw new InvalidArgumentException("Undefined relative for listener '$id': $relative."); |
||
202 | } |
||
203 | } |
||
204 | |||
205 | /** |
||
206 | * @param array<string, mixed> $attributes |
||
207 | * |
||
208 | * @return int|string |
||
209 | */ |
||
210 | private function extractPriority(array $attributes, string $id) |
||
211 | { |
||
212 | $priority = $attributes[self::ATTRIBUTE_PRIORITY] ?? self::DEFAULT_PRIORITY; |
||
213 | |||
214 | if ($priority !== self::PRIORITY_FIRST && $priority !== self::PRIORITY_LAST && !is_int($priority)) { |
||
215 | throw new InvalidArgumentException( |
||
216 | "Invalid priority value for listener '$id': $priority." |
||
217 | . " Valid values are 'first', 'last', or an integer." |
||
218 | ); |
||
219 | } |
||
220 | |||
221 | return $priority; |
||
222 | } |
||
223 | |||
224 | /** |
||
225 | * @param array<class-string, string[]> $mapping |
||
226 | * Where _key_ is an Event and _value_ is an array of service identifiers. |
||
227 | * @param array<class-string, array<string, int|string>> $prioritiesByEvent |
||
228 | * Where _key_ is an Event and _value_ is an array where _key_ is a service identifier and _value_ a priority. |
||
229 | * @param array<class-string, array<string, string>> $beforeByEvent |
||
230 | * Where _key_ is an Event and _value_ is an array where _key_ and _value_ are both service identifiers. |
||
231 | * @param array<class-string, array<string, string>> $afterByEvent |
||
232 | * Where _key_ is an Event and _value_ is an array where _key_ and _value_ are both service identifiers. |
||
233 | * |
||
234 | * @return array<class-string, string[]> |
||
235 | * Where _key_ is an Event and _value_ is an array of service identifiers, sorted. |
||
236 | */ |
||
237 | private function sortMapping( |
||
238 | array $mapping, |
||
239 | array $prioritiesByEvent, |
||
240 | array $beforeByEvent, |
||
241 | array $afterByEvent |
||
242 | ): array { |
||
243 | foreach ($mapping as $event => &$listeners) { |
||
244 | $listeners = $this->sortListeners( |
||
245 | $listeners, |
||
246 | $prioritiesByEvent[$event] ?? [], |
||
247 | $beforeByEvent[$event] ?? [], |
||
248 | $afterByEvent[$event] ?? [] |
||
249 | ); |
||
250 | } |
||
251 | |||
252 | return $mapping; |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * @param string[] $listeners |
||
257 | * An array of service identifiers. |
||
258 | * @param array<string, int|string> $priorities |
||
259 | * Where _key_ is a service identifier and _value_ a priority. |
||
260 | * @param array<string, string> $beforePositions |
||
261 | * Where _key_ and _value_ are both service identifiers. |
||
262 | * @param array<string, string> $afterPositions |
||
263 | * Where _key_ and _value_ are both service identifiers. |
||
264 | * |
||
265 | * @return string[] |
||
266 | * An array of service identifiers, sorted. |
||
267 | */ |
||
268 | private function sortListeners( |
||
269 | array $listeners, |
||
270 | array $priorities, |
||
271 | array $beforePositions, |
||
272 | array $afterPositions |
||
273 | ): array { |
||
274 | $priorities = $this->resolvePriorities($priorities); |
||
275 | $listenersByPosition = array_flip($listeners); |
||
276 | $listenersWithPriority = array_intersect_key($priorities, $listenersByPosition); |
||
277 | $sortedListeners = array_fill_keys($this->sortListenersWithPriority($listenersWithPriority), 0); |
||
278 | |||
279 | while ($beforePositions || $afterPositions) { |
||
2 ignored issues
–
show
|
|||
280 | $inserted = 0; |
||
281 | |||
282 | foreach ($beforePositions as $id => $relative) { |
||
283 | if (isset($sortedListeners[$relative])) { |
||
284 | $this->insertBeforeListener($sortedListeners, $id, $relative); |
||
285 | $inserted++; |
||
286 | unset($beforePositions[$id]); |
||
287 | } |
||
288 | } |
||
289 | |||
290 | foreach ($afterPositions as $id => $relative) { |
||
291 | if (isset($sortedListeners[$relative])) { |
||
292 | $this->insertAfterListener($sortedListeners, $id, $relative); |
||
293 | $inserted++; |
||
294 | unset($afterPositions[$id]); |
||
295 | } |
||
296 | } |
||
297 | |||
298 | if (!$inserted) { |
||
299 | throw new LogicException(sprintf( |
||
300 | "Unable to insert the following listeners: %s. Please check the position logic.", |
||
301 | implode(', ', array_merge(array_keys($beforePositions), array_keys($afterPositions))) |
||
302 | )); |
||
303 | } |
||
304 | } |
||
305 | |||
306 | // @phpstan-ignore-next-line |
||
307 | return array_keys($sortedListeners); |
||
308 | } |
||
309 | |||
310 | /** |
||
311 | * @param array<string, string|int> $priorities |
||
312 | * |
||
313 | * @return array<string, int> |
||
314 | */ |
||
315 | private function resolvePriorities(array $priorities): array |
||
316 | { |
||
317 | $numericPriorities = array_filter($priorities, 'is_int'); |
||
318 | if (!$numericPriorities) { |
||
1 ignored issue
–
show
|
|||
319 | return []; |
||
320 | } |
||
321 | |||
322 | $min = min($numericPriorities); |
||
323 | $max = max($numericPriorities); |
||
324 | |||
325 | foreach ($priorities as &$priority) { |
||
326 | if ($priority === self::PRIORITY_FIRST) { |
||
327 | $priority = ++$max; |
||
328 | } elseif ($priority === self::PRIORITY_LAST) { |
||
329 | $priority = --$min; |
||
330 | } |
||
331 | } |
||
332 | |||
333 | // @phpstan-ignore-next-line |
||
334 | return $priorities; |
||
335 | } |
||
336 | |||
337 | /** |
||
338 | * @param array<string, int> $listenersWithPriority |
||
339 | * |
||
340 | * @return string[] |
||
341 | */ |
||
342 | private function sortListenersWithPriority(array $listenersWithPriority): array |
||
343 | { |
||
344 | $listeners = array_keys($listenersWithPriority); |
||
345 | $positions = array_flip($listeners); |
||
346 | |||
347 | usort( |
||
348 | $listeners, |
||
349 | function (string $a, string $b) use ($listenersWithPriority, $positions): int { |
||
350 | $pa = $listenersWithPriority[$a]; |
||
351 | $pb = $listenersWithPriority[$b]; |
||
352 | |||
353 | if ($pa === $pb) { |
||
354 | // Same priority. Let's compare the original orders, which are ascending. |
||
355 | return $positions[$a] <=> $positions[$b]; |
||
356 | } |
||
357 | |||
358 | // Priorities are descending. |
||
359 | return $pb <=> $pa; |
||
360 | } |
||
361 | ); |
||
362 | |||
363 | return $listeners; |
||
364 | } |
||
365 | |||
366 | /** |
||
367 | * @param array<string, mixed> $listeners Where _key_ is a service identifier. |
||
368 | */ |
||
369 | public function insertBeforeListener(array &$listeners, string $id, string $relative): void |
||
370 | { |
||
371 | $positions = array_flip(array_keys($listeners)); |
||
372 | $p = $positions[$relative]; |
||
373 | |||
374 | if ($p === 0) { |
||
375 | $listeners = [ $id => 0 ] + $listeners; |
||
376 | |||
377 | return; |
||
378 | } |
||
379 | |||
380 | $listeners = array_slice($listeners, 0, $p) |
||
381 | + [ $id => true ] |
||
382 | + array_slice($listeners, $p); |
||
383 | } |
||
384 | |||
385 | /** |
||
386 | * @param array<string, mixed> $listeners Where _key_ is a service identifier. |
||
387 | */ |
||
388 | public function insertAfterListener(array &$listeners, string $id, string $relative): void |
||
402 | } |
||
403 | } |
||
404 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.