Total Complexity | 185 |
Total Lines | 616 |
Duplicated Lines | 0 % |
Changes | 8 | ||
Bugs | 1 | Features | 0 |
Complex classes like Resolver 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 Resolver, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
31 | class Resolver |
||
32 | { |
||
33 | private AbstractContainer $container; |
||
34 | private ?BuilderFactory $builder; |
||
35 | private bool $strict = true; |
||
36 | |||
37 | /** @var array<string,\PhpParser\Node> */ |
||
38 | private array $literalCache = []; |
||
39 | |||
40 | public function __construct(AbstractContainer $container, BuilderFactory $builder = null) |
||
41 | { |
||
42 | $this->builder = $builder; |
||
43 | $this->container = $container; |
||
44 | } |
||
45 | |||
46 | /** |
||
47 | * The method name generated for a service definition. |
||
48 | */ |
||
49 | public static function createMethod(string $id): string |
||
50 | { |
||
51 | return 'get' . \str_replace(['.', '_', '\\'], '', \ucwords($id, '._')); |
||
52 | } |
||
53 | |||
54 | /** |
||
55 | * If true, exception will be thrown on resolvable services with are not typed. |
||
56 | */ |
||
57 | public function setStrictAutowiring(bool $boolean = true): void |
||
58 | { |
||
59 | $this->strict = $boolean; |
||
60 | } |
||
61 | |||
62 | /** |
||
63 | * @param mixed $definition |
||
64 | */ |
||
65 | public static function autowireService($definition, bool $allTypes = false, AbstractContainer $container = null): array |
||
66 | { |
||
67 | $types = $autowired = []; |
||
68 | |||
69 | if (\is_callable($definition)) { |
||
70 | $types = \array_filter(self::getTypes(Callback::toReflection($definition)), fn (string $v) => \class_exists($v) || \interface_exists($v) || $allTypes); |
||
71 | } elseif (\is_object($definition)) { |
||
72 | if ($definition instanceof \stdClass) { |
||
73 | return $allTypes ? ['object'] : $types; |
||
74 | } |
||
75 | |||
76 | $types[] = \get_class($definition); |
||
77 | } elseif (\is_string($definition)) { |
||
78 | if (!(\class_exists($definition) || \interface_exists($definition))) { |
||
79 | return $allTypes ? ['string'] : []; |
||
80 | } |
||
81 | |||
82 | $types[] = $definition; |
||
83 | } elseif (\is_array($definition)) { |
||
84 | if (null !== $container && 2 === \count($definition, \COUNT_RECURSIVE)) { |
||
85 | if ($definition[0] instanceof Definitions\Reference) { |
||
86 | $def = $container->definition((string) $definition[0]); |
||
87 | } elseif ($definition[0] instanceof Expr\BinaryOp\Coalesce) { |
||
88 | $def = $container->definition($definition[0]->left->dim->value); |
||
89 | } |
||
90 | |||
91 | if (isset($def)) { |
||
92 | if ($def instanceof Definitions\DefinitionInterface) { |
||
93 | $class = self::getDefinitionClass($def); |
||
94 | |||
95 | if (null === $class) { |
||
96 | return []; |
||
97 | } |
||
98 | } |
||
99 | $types = self::getTypes(new \ReflectionMethod($class ?? $def, $definition[1])); |
||
100 | goto resolve_types; |
||
101 | } |
||
102 | } |
||
103 | |||
104 | return $allTypes ? ['array'] : []; |
||
105 | } |
||
106 | |||
107 | resolve_types: |
||
108 | foreach ($types as $type) { |
||
109 | $autowired[] = $type; |
||
110 | |||
111 | foreach (\class_implements($type) ?: [] as $interface) { |
||
112 | $autowired[] = $interface; |
||
113 | } |
||
114 | |||
115 | foreach (\class_parents($type) ?: [] as $parent) { |
||
116 | $autowired[] = $parent; |
||
117 | } |
||
118 | } |
||
119 | |||
120 | return $autowired; |
||
121 | } |
||
122 | |||
123 | /** |
||
124 | * Resolves arguments for callable. |
||
125 | * |
||
126 | * @param array<int|string,mixed> $args |
||
127 | * |
||
128 | * @return array<int,mixed> |
||
129 | */ |
||
130 | public function autowireArguments(\ReflectionFunctionAbstract $function, array $args = []): array |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * Resolve a service definition, class string, invocable object or callable |
||
178 | * using autowiring. |
||
179 | * |
||
180 | * @param string|callable|object $callback |
||
181 | * @param array<int|string,mixed> $args |
||
182 | * |
||
183 | * @throws ContainerResolutionException|\ReflectionException if unresolvable |
||
184 | * |
||
185 | * @return mixed |
||
186 | */ |
||
187 | public function resolve($callback, array $args = []) |
||
188 | { |
||
189 | if ($callback instanceof Definitions\Statement) { |
||
190 | $resolved = $this->resolve($callback->getValue(), $callback->getArguments() + $args); |
||
191 | |||
192 | if ($callback->isClosureWrappable()) { |
||
193 | $resolved = null === $this->builder ? fn () => $resolved : new Expr\ArrowFunction(['expr' => $resolved]); |
||
194 | } |
||
195 | } elseif ($callback instanceof Definitions\Reference) { |
||
196 | $resolved = $this->resolveReference((string) $callback); |
||
197 | |||
198 | if (\is_callable($resolved) || (\is_array($resolved) && 2 === \count($resolved, \COUNT_RECURSIVE))) { |
||
199 | $resolved = $this->resolveCallable($resolved, $args); |
||
200 | } else { |
||
201 | $callback = $resolved; |
||
202 | } |
||
203 | } elseif ($callback instanceof Definitions\ValueDefinition) { |
||
204 | $resolved = $callback->getEntity(); |
||
205 | } elseif ($callback instanceof Definitions\TaggedLocator) { |
||
206 | $resolved = $this->resolve($callback->resolve($this->container)); |
||
207 | } elseif ($callback instanceof Builder\PhpLiteral) { |
||
208 | $expression = $this->literalCache[\spl_object_id($callback)] ??= $callback->resolve($this)[0]; |
||
209 | $resolved = $expression instanceof Stmt\Expression ? $expression->expr : $expression; |
||
210 | } elseif (Services\ServiceLocator::class === $callback) { |
||
211 | $services = []; |
||
212 | |||
213 | foreach ($args as $name => $service) { |
||
214 | $services += $this->resolveServiceSubscriber($name, (string) $service); |
||
215 | } |
||
216 | $resolved = null === $this->builder ? new Services\ServiceLocator($services) : $this->builder->new('\\' . Services\ServiceLocator::class, [$services]); |
||
217 | } elseif (\is_string($callback)) { |
||
218 | if (\str_contains($callback, '%')) { |
||
219 | $callback = $this->container->parameter($callback); |
||
220 | } |
||
221 | |||
222 | if (\class_exists($callback)) { |
||
223 | return $this->resolveClass($callback, $args); |
||
224 | } |
||
225 | |||
226 | if (\is_callable($callback)) { |
||
227 | $resolved = $this->resolveCallable($callback, $args); |
||
228 | } |
||
229 | } elseif (\is_callable($callback) || \is_array($callback)) { |
||
230 | $resolved = $this->resolveCallable($callback, $args); |
||
231 | } |
||
232 | |||
233 | return $resolved ?? (null === $this->builder ? $callback : $this->builder->val($callback)); |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Resolves callables and array like callables. |
||
238 | * |
||
239 | * @param callable|array<int,mixed> $callback |
||
240 | * @param array<int|string,mixed> $arguments |
||
241 | * |
||
242 | * @throws \ReflectionException if $callback is not a real callable |
||
243 | * |
||
244 | * @return mixed |
||
245 | */ |
||
246 | public function resolveCallable($callback, array $arguments = []) |
||
247 | { |
||
248 | if (\is_array($callback)) { |
||
249 | if (2 === \count($callback, \COUNT_RECURSIVE) && \is_string($callback[1])) { |
||
250 | $callback[0] = $this->resolve($callback[0]); |
||
251 | |||
252 | if ($callback[0] instanceof Expr\BinaryOp\Coalesce) { |
||
253 | $class = self::getDefinitionClass($this->container->definition($callback[0]->left->dim->value)); |
||
254 | |||
255 | if (null !== $class) { |
||
256 | $type = [$class, $callback[1]]; |
||
257 | } |
||
258 | } elseif ($callback[0] instanceof Expr\New_) { |
||
259 | $type = [(string) $callback[0]->class, $callback[1]]; |
||
260 | } |
||
261 | |||
262 | if (isset($type) || \is_callable($callback)) { |
||
263 | goto create_callable; |
||
264 | } |
||
265 | } |
||
266 | |||
267 | $callback = $this->resolveArguments($callback); |
||
268 | |||
269 | return null === $this->builder ? $callback : $this->builder->val($callback); |
||
270 | } |
||
271 | |||
272 | create_callable: |
||
273 | $args = $this->autowireArguments($ref = Callback::toReflection($type ?? $callback), $arguments); |
||
274 | |||
275 | if ($ref instanceof \ReflectionFunction) { |
||
276 | return null === $this->builder ? $ref->invokeArgs($args) : $this->builder->funcCall($callback, $args); |
||
277 | } |
||
278 | |||
279 | if ($ref->isStatic()) { |
||
280 | $className = \is_array($callback) ? $callback[0] : $ref->getDeclaringClass()->getName(); |
||
281 | |||
282 | return null === $this->builder ? $ref->invokeArgs(null, $args) : $this->builder->staticCall($className, $ref->getName(), $args); |
||
283 | } |
||
284 | |||
285 | return null === $this->builder ? $callback(...$args) : $this->builder->methodCall($callback[0], $ref->getName(), $args); |
||
286 | } |
||
287 | |||
288 | /** |
||
289 | * @param array<int|string,mixed> $args |
||
290 | * |
||
291 | * @throws ContainerResolutionException|\ReflectionException if class string unresolvable |
||
292 | */ |
||
293 | public function resolveClass(string $class, array $args = []): object |
||
294 | { |
||
295 | /** @var class-string $class */ |
||
296 | $reflection = new \ReflectionClass($class); |
||
297 | |||
298 | if ($reflection->isAbstract() || !$reflection->isInstantiable()) { |
||
299 | throw new ContainerResolutionException(\sprintf('Class %s is an abstract type or instantiable.', $class)); |
||
300 | } |
||
301 | |||
302 | if (null === $constructor = $reflection->getConstructor()) { |
||
303 | if (!empty($args)) { |
||
304 | throw new ContainerResolutionException(\sprintf('Unable to pass arguments, class "%s" has no constructor.', $class)); |
||
305 | } |
||
306 | |||
307 | $service = null === $this->builder ? $reflection->newInstanceWithoutConstructor() : $this->builder->new($class); |
||
308 | } else { |
||
309 | $args = $this->autowireArguments($constructor, $args); |
||
310 | $service = null === $this->builder ? $reflection->newInstanceArgs($args) : $this->builder->new($class, $args); |
||
311 | } |
||
312 | |||
313 | if ($reflection->implementsInterface(Injector\InjectableInterface::class)) { |
||
314 | return Injector\Injectable::getProperties($this, $service, $reflection); |
||
315 | } |
||
316 | |||
317 | return $service; |
||
318 | } |
||
319 | |||
320 | /** |
||
321 | * @param array<int|string,mixed> $arguments |
||
322 | * |
||
323 | * @return array<int|string,mixed> |
||
324 | */ |
||
325 | public function resolveArguments(array $arguments = []): array |
||
326 | { |
||
327 | foreach ($arguments as $key => $value) { |
||
328 | if ($value instanceof \stdClass) { |
||
329 | $resolved = null === $this->builder ? $value : new Expr\Cast\Object_($this->builder->val($this->resolveArguments((array) $value))); |
||
330 | } elseif (\is_array($value)) { |
||
331 | $resolved = $this->resolveArguments($value); |
||
332 | } elseif (\is_int($value)) { |
||
333 | $resolved = null === $this->builder ? $value : new Scalar\LNumber($value); |
||
334 | } elseif (\is_float($value)) { |
||
335 | $resolved = null === $this->builder ? (int) $value : new Scalar\DNumber($value); |
||
336 | } elseif (\is_numeric($value)) { |
||
337 | $resolved = null === $this->builder ? (int) $value : Scalar\LNumber::fromString($value); |
||
338 | } elseif (\is_string($value)) { |
||
339 | if (\str_contains($value, '%')) { |
||
340 | $value = $this->container->parameter($value); |
||
341 | } |
||
342 | |||
343 | $resolved = null === $this->builder ? $value : $this->builder->val($value); |
||
344 | } else { |
||
345 | $resolved = $this->resolve($value); |
||
346 | } |
||
347 | |||
348 | $arguments[$key] = $resolved; |
||
349 | } |
||
350 | |||
351 | return $arguments; |
||
352 | } |
||
353 | |||
354 | /** |
||
355 | * Resolves service by type. |
||
356 | * |
||
357 | * @param string $id A class or an interface name |
||
358 | * |
||
359 | * @return mixed |
||
360 | */ |
||
361 | public function get(string $id, bool $single = false) |
||
362 | { |
||
363 | if (\is_subclass_of($id, ServiceSubscriberInterface::class)) { |
||
364 | static $services = []; |
||
365 | |||
366 | foreach ($id::getSubscribedServices() as $name => $service) { |
||
367 | $services += $this->resolveServiceSubscriber($name, $service); |
||
368 | } |
||
369 | |||
370 | if (null === $builder = $this->builder) { |
||
371 | return new Services\ServiceLocator($services); |
||
372 | } |
||
373 | |||
374 | return $builder->new('\\' . Services\ServiceLocator::class, [$services]); |
||
375 | } |
||
376 | |||
377 | if (!$this->strict) { |
||
378 | return $this->container->get($id, $single ? $this->container::EXCEPTION_ON_MULTIPLE_SERVICE : $this->container::IGNORE_MULTIPLE_SERVICE); |
||
379 | } |
||
380 | |||
381 | if ($this->container->typed($id)) { |
||
382 | return $this->container->autowired($id, $single); |
||
383 | } |
||
384 | |||
385 | throw new NotFoundServiceException(\sprintf('Service of type "%s" not found. Check class name because it cannot be found.', $id)); |
||
386 | } |
||
387 | |||
388 | /** |
||
389 | * Gets the PHP's parser builder. |
||
390 | */ |
||
391 | public function getBuilder(): ?BuilderFactory |
||
392 | { |
||
393 | return $this->builder; |
||
394 | } |
||
395 | |||
396 | /** |
||
397 | * @return mixed |
||
398 | */ |
||
399 | private function resolveReference(string $reference) |
||
400 | { |
||
401 | if ('?' === $reference[0]) { |
||
402 | $invalidBehavior = $this->container::EXCEPTION_ON_MULTIPLE_SERVICE; |
||
403 | $reference = \substr($reference, 1); |
||
404 | |||
405 | if ($arrayLike = \str_ends_with('[]', $reference)) { |
||
406 | $reference = \substr($reference, 0, -2); |
||
407 | $invalidBehavior = $this->container::IGNORE_MULTIPLE_SERVICE; |
||
408 | } |
||
409 | |||
410 | if ($this->container->has($reference) || $this->container->typed($reference)) { |
||
411 | return $this->container->get($reference, $invalidBehavior); |
||
412 | } |
||
413 | |||
414 | return $arrayLike ? [] : null; |
||
415 | } |
||
416 | |||
417 | if ('[]' === \substr($reference, -2)) { |
||
418 | return $this->container->get(\substr($reference, 0, -2), $this->container::IGNORE_MULTIPLE_SERVICE); |
||
419 | } |
||
420 | |||
421 | return $this->container->get($reference); |
||
422 | } |
||
423 | |||
424 | /** |
||
425 | * Resolves services for ServiceLocator. |
||
426 | * |
||
427 | * @param int|string $id |
||
428 | * |
||
429 | * @return (\Closure|array|mixed|null)[] |
||
430 | */ |
||
431 | private function resolveServiceSubscriber($id, string $value): array |
||
432 | { |
||
433 | if ('?' === $value[0]) { |
||
434 | $arrayLike = \str_ends_with($value = \substr($value, 1), '[]'); |
||
435 | |||
436 | if (\is_int($id)) { |
||
437 | $id = $arrayLike ? \substr($value, 0, -2) : $value; |
||
438 | } |
||
439 | |||
440 | return ($this->container->has($id) || $this->container->typed($id)) ? $this->resolveServiceSubscriber($id, $value) : [$id => $arrayLike ? [] : null]; |
||
441 | } |
||
442 | |||
443 | $service = function () use ($value) { |
||
444 | if ('[]' === \substr($value, -2)) { |
||
445 | $service = $this->container->get(\substr($value, 0, -2), $this->container::IGNORE_MULTIPLE_SERVICE); |
||
446 | |||
447 | return \is_array($service) ? $service : [$service]; |
||
448 | } |
||
449 | |||
450 | return $this->container->get($value); |
||
451 | }; |
||
452 | |||
453 | if (null !== $this->builder) { |
||
454 | if ('[]' === \substr($value, -2)) { |
||
455 | $returnType = 'array'; |
||
456 | } elseif ($this->container->has($value) && ($def = $this->container->definition($value)) instanceof Definitions\TypedDefinitionInterface) { |
||
457 | $returnType = $def->getTypes()[0] ?? ( |
||
458 | \class_exists($value) || \interface_exists($value) |
||
459 | ? $value |
||
460 | : (!\is_int($id) && (\class_exists($id) || \interface_exists($id)) ? $id : null) |
||
461 | ); |
||
462 | } elseif (\class_exists($value) || \interface_exists($value)) { |
||
463 | $returnType = $value; |
||
464 | } |
||
465 | |||
466 | $service = new Expr\ArrowFunction(['expr' => $this->builder->val($service()), 'returnType' => $returnType ?? null]); |
||
467 | } |
||
468 | |||
469 | return [\is_int($id) ? \rtrim($value, '[]') : $id => $service]; |
||
470 | } |
||
471 | |||
472 | /** |
||
473 | * Resolves missing argument using autowiring. |
||
474 | * |
||
475 | * @param array<int|string,mixed> $providedParameters |
||
476 | * @param array<int,string> $types |
||
477 | * |
||
478 | * @throws ContainerResolutionException |
||
479 | * |
||
480 | * @return mixed |
||
481 | */ |
||
482 | private function autowireArgument(\ReflectionParameter $parameter, array $types, array $providedParameters) |
||
483 | { |
||
484 | foreach ($types as $typeName) { |
||
485 | if (!Reflection::isBuiltinType($typeName)) { |
||
486 | try { |
||
487 | return $providedParameters[$typeName] ?? $this->get($typeName, !$parameter->isVariadic()); |
||
488 | } catch (NotFoundServiceException $e) { |
||
489 | // Ignore this exception ... |
||
490 | } catch (ContainerResolutionException $e) { |
||
491 | $errorException = new ContainerResolutionException(\sprintf("{$e->getMessage()} (needed by %s)", Reflection::toString($parameter))); |
||
492 | } |
||
493 | |||
494 | if ( |
||
495 | ServiceProviderInterface::class === $typeName && |
||
496 | null !== $class = $parameter->getDeclaringClass() |
||
497 | ) { |
||
498 | if (!$class->implementsInterface(ServiceSubscriberInterface::class)) { |
||
499 | throw new ContainerResolutionException(\sprintf( |
||
500 | 'Service of type %s needs parent class %s to implement %s.', |
||
501 | $typeName, |
||
502 | $class->getName(), |
||
503 | ServiceSubscriberInterface::class |
||
504 | )); |
||
505 | } |
||
506 | |||
507 | return $this->get($class->getName()); |
||
508 | } |
||
509 | } |
||
510 | |||
511 | if (\PHP_MAJOR_VERSION >= 8 && $attributes = $parameter->getAttributes()) { |
||
512 | foreach ($attributes as $attribute) { |
||
513 | if (Attribute\Inject::class === $attribute->getName()) { |
||
514 | if (null === $attrName = $attribute->getArguments()[0] ?? null) { |
||
515 | throw new ContainerResolutionException(\sprintf('Using the Inject attribute on parameter %s requires a value to be set.', $parameter->getName())); |
||
516 | } |
||
517 | |||
518 | if ($arrayLike = \str_ends_with($attrName, '[]')) { |
||
519 | $attrName = \substr($attrName, 0, -2); |
||
520 | } |
||
521 | |||
522 | try { |
||
523 | return $this->get($attrName, !$arrayLike); |
||
524 | } catch (NotFoundServiceException $e) { |
||
525 | // Ignore this exception ... |
||
526 | } |
||
527 | } |
||
528 | } |
||
529 | } |
||
530 | |||
531 | if ( |
||
532 | ($method = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod |
||
533 | && \preg_match('#@param[ \t]+([\w\\\\]+)(?:\[\])?[ \t]+\$' . $parameter->name . '#', (string) $method->getDocComment(), $m) |
||
534 | && ($itemType = Reflection::expandClassName($m[1], $method->getDeclaringClass())) |
||
535 | && (\class_exists($itemType) || \interface_exists($itemType)) |
||
536 | ) { |
||
537 | try { |
||
538 | if (\in_array($typeName, ['array', 'iterable'], true)) { |
||
539 | return $this->get($itemType); |
||
540 | } |
||
541 | |||
542 | if ('object' === $typeName || \is_subclass_of($itemType, $typeName)) { |
||
543 | return $this->get($itemType, true); |
||
544 | } |
||
545 | } catch (NotFoundServiceException $e) { |
||
546 | // Ignore this exception ... |
||
547 | } |
||
548 | } |
||
549 | |||
550 | if (isset($errorException)) { |
||
551 | throw $errorException; |
||
552 | } |
||
553 | } |
||
554 | |||
555 | return null; |
||
556 | } |
||
557 | |||
558 | /** |
||
559 | * Returns an associated type to the given parameter if available. |
||
560 | * |
||
561 | * @param \ReflectionParameter|\ReflectionFunctionAbstract $reflection |
||
562 | * |
||
563 | * @return array<int,string> |
||
564 | */ |
||
565 | private static function getTypes(\Reflector $reflection): array |
||
600 | } |
||
601 | |||
602 | private static function getDefinitionClass(Definitions\DefinitionInterface $def): ?string |
||
603 | { |
||
604 | if (!\class_exists($class = $def->getEntity())) { |
||
605 | if ($def instanceof Definitions\TypedDefinitionInterface) { |
||
606 | foreach ($def->getTypes() as $typed) { |
||
607 | if (\class_exists($typed)) { |
||
608 | return $typed; |
||
609 | } |
||
610 | } |
||
611 | } |
||
612 | |||
613 | return null; |
||
614 | } |
||
615 | |||
616 | return $class; |
||
617 | } |
||
618 | |||
619 | /** |
||
620 | * Get the parameter's allowed null else error. |
||
621 | * |
||
622 | * @throws \ReflectionException|ContainerResolutionException |
||
623 | * |
||
624 | * @return null|void |
||
625 | */ |
||
626 | private static function getParameterDefaultValue(\ReflectionParameter $parameter, array $types) |
||
647 | } |
||
648 | } |
||
649 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths