Total Complexity | 186 |
Total Lines | 623 |
Duplicated Lines | 0 % |
Changes | 18 | ||
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 |
||
32 | class Resolver |
||
33 | { |
||
34 | private AbstractContainer $container; |
||
35 | private ?BuilderFactory $builder; |
||
36 | private bool $strict = true; |
||
37 | |||
38 | /** @var array<string,\PhpParser\Node> */ |
||
39 | private array $literalCache = []; |
||
40 | |||
41 | public function __construct(AbstractContainer $container, BuilderFactory $builder = null) |
||
42 | { |
||
43 | $this->builder = $builder; |
||
44 | $this->container = $container; |
||
45 | } |
||
46 | |||
47 | /** |
||
48 | * The method name generated for a service definition. |
||
49 | */ |
||
50 | public static function createMethod(string $id): string |
||
51 | { |
||
52 | return 'get' . \str_replace(['.', '_', '\\'], '', \ucwords($id, '._')); |
||
53 | } |
||
54 | |||
55 | /** |
||
56 | * If true, exception will be thrown on resolvable services with are not typed. |
||
57 | */ |
||
58 | public function setStrictAutowiring(bool $boolean = true): void |
||
59 | { |
||
60 | $this->strict = $boolean; |
||
61 | } |
||
62 | |||
63 | /** |
||
64 | * @param mixed $definition |
||
65 | */ |
||
66 | public static function autowireService($definition, bool $allTypes = false, AbstractContainer $container = null): array |
||
67 | { |
||
68 | $types = $autowired = []; |
||
69 | |||
70 | if (\is_callable($definition)) { |
||
71 | $types = \array_filter(self::getTypes(Callback::toReflection($definition)), fn (string $v) => \class_exists($v) || \interface_exists($v) || $allTypes); |
||
72 | } elseif (\is_object($definition)) { |
||
73 | if ($definition instanceof \stdClass) { |
||
74 | return $allTypes ? ['object'] : $types; |
||
75 | } |
||
76 | |||
77 | $types[] = \get_class($definition); |
||
78 | } elseif (\is_string($definition)) { |
||
79 | if (!(\class_exists($definition) || \interface_exists($definition))) { |
||
80 | return $allTypes ? ['string'] : []; |
||
81 | } |
||
82 | |||
83 | $types[] = $definition; |
||
84 | } elseif (\is_array($definition)) { |
||
85 | if (null !== $container && 2 === \count($definition, \COUNT_RECURSIVE)) { |
||
86 | if ($definition[0] instanceof Definitions\Reference) { |
||
87 | $def = $container->definition((string) $definition[0]); |
||
88 | } elseif ($definition[0] instanceof Expr\BinaryOp\Coalesce) { |
||
89 | $def = $container->definition($definition[0]->left->dim->value); |
||
90 | } |
||
91 | |||
92 | if (isset($def)) { |
||
93 | if ($def instanceof Definitions\DefinitionInterface) { |
||
94 | $class = self::getDefinitionClass($def); |
||
95 | |||
96 | if (null === $class) { |
||
97 | return []; |
||
98 | } |
||
99 | } |
||
100 | $types = self::getTypes(new \ReflectionMethod($class ?? $def, $definition[1])); |
||
101 | goto resolve_types; |
||
102 | } |
||
103 | } |
||
104 | |||
105 | return $allTypes ? ['array'] : []; |
||
106 | } |
||
107 | |||
108 | resolve_types: |
||
109 | foreach ($types as $type) { |
||
110 | $autowired[] = $type; |
||
111 | |||
112 | foreach (\class_implements($type) ?: [] as $interface) { |
||
113 | $autowired[] = $interface; |
||
114 | } |
||
115 | |||
116 | foreach (\class_parents($type) ?: [] as $parent) { |
||
117 | $autowired[] = $parent; |
||
118 | } |
||
119 | } |
||
120 | |||
121 | return $autowired; |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * Resolves arguments for callable. |
||
126 | * |
||
127 | * @param array<int|string,mixed> $args |
||
128 | * |
||
129 | * @return array<int,mixed> |
||
130 | */ |
||
131 | public function autowireArguments(\ReflectionFunctionAbstract $function, array $args = []): array |
||
132 | { |
||
133 | $resolvedParameters = []; |
||
134 | $nullValuesFound = 0; |
||
135 | $args = $this->resolveArguments($args); // Resolves provided arguments. |
||
136 | |||
137 | foreach ($function->getParameters() as $offset => $parameter) { |
||
138 | $position = 0 === $nullValuesFound ? $offset : $parameter->name; |
||
139 | $resolved = $args[$offset] ?? $args[$parameter->name] ?? null; |
||
140 | $types = self::getTypes($parameter) ?: ['null']; |
||
141 | |||
142 | if (\PHP_VERSION_ID >= 80100 && (\count($types) >= 1 && \is_subclass_of($enumType = $types[0], \BackedEnum::class))) { |
||
|
|||
143 | if (null === $resolved = ($resolved ?? $args[$enumType] ?? null)) { |
||
144 | throw new ContainerResolutionException(\sprintf('Missing value for enum parameter %s.', Reflection::toString($parameter))); |
||
145 | } |
||
146 | |||
147 | try { |
||
148 | $resolvedParameters[$position] = $enumType::from($resolved); |
||
149 | } catch (\ValueError $e) { |
||
150 | throw new ContainerResolutionException(\sprintf('The "%s" value could not be resolved for enum parameter %s.', $resolved, Reflection::toString($parameter)), 0, $e); |
||
151 | } |
||
152 | continue; |
||
153 | } |
||
154 | |||
155 | if (null === ($resolved = $resolved ?? $this->autowireArgument($parameter, $types, $args))) { |
||
156 | if ($parameter->isDefaultValueAvailable()) { |
||
157 | if (\PHP_MAJOR_VERSION < 8) { |
||
158 | $resolvedParameters[$position] = Reflection::getParameterDefaultValue($parameter); |
||
159 | } else { |
||
160 | ++$nullValuesFound; |
||
161 | } |
||
162 | } elseif (!$parameter->isVariadic()) { |
||
163 | $resolvedParameters[$position] = self::getParameterDefaultValue($parameter, $types); |
||
164 | } |
||
165 | |||
166 | continue; |
||
167 | } |
||
168 | |||
169 | if ($parameter->isVariadic() && \is_array($resolved)) { |
||
170 | $resolvedParameters = \array_merge($resolvedParameters, $resolved); |
||
171 | continue; |
||
172 | } |
||
173 | |||
174 | $resolvedParameters[$position] = $resolved; |
||
175 | } |
||
176 | |||
177 | return $resolvedParameters; |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * Resolve a service definition, class string, invocable object or callable |
||
182 | * using autowiring. |
||
183 | * |
||
184 | * @param string|callable|object $callback |
||
185 | * @param array<int|string,mixed> $args |
||
186 | * |
||
187 | * @throws ContainerResolutionException|\ReflectionException if unresolvable |
||
188 | * |
||
189 | * @return mixed |
||
190 | */ |
||
191 | public function resolve($callback, array $args = []) |
||
192 | { |
||
193 | if ($callback instanceof Definitions\Parameter) { |
||
194 | if (!\array_key_exists($param = (string) $callback, $this->container->parameters)) { |
||
195 | if (!$callback->shouldResolve()) { |
||
196 | throw new ContainerResolutionException(\sprintf('The parameter "%s" is not defined.', $param)); |
||
197 | } |
||
198 | |||
199 | return null === $this->builder ? $this->container->parameter($param) : $this->builder->methodCall(new Expr\Variable('this'), 'parameter', [$param]); |
||
200 | } |
||
201 | |||
202 | if (null !== $this->builder) { |
||
203 | return $resolved = new Expr\ArrayDimFetch($this->builder->propertyFetch(new Expr\Variable('this'), 'parameters'), new String_($param)); |
||
204 | } |
||
205 | |||
206 | $resolved = $this->container->parameters[$param]; |
||
207 | } elseif ($callback instanceof Definitions\Statement) { |
||
208 | $resolved = $this->resolve($callback->getValue(), $callback->getArguments() + $args); |
||
209 | |||
210 | if ($callback->isClosureWrappable()) { |
||
211 | $resolved = null === $this->builder ? fn () => $resolved : new Expr\ArrowFunction(['expr' => $resolved]); |
||
212 | } |
||
213 | } elseif ($callback instanceof Definitions\Reference) { |
||
214 | $resolved = $this->resolveReference((string) $callback); |
||
215 | |||
216 | if (\is_callable($resolved) || (\is_array($resolved) && 2 === \count($resolved, \COUNT_RECURSIVE))) { |
||
217 | $resolved = $this->resolveCallable($resolved, $args); |
||
218 | } else { |
||
219 | $callback = $resolved; |
||
220 | } |
||
221 | } elseif ($callback instanceof Definitions\ValueDefinition) { |
||
222 | $resolved = $callback->getEntity(); |
||
223 | } elseif ($callback instanceof Definitions\TaggedLocator) { |
||
224 | $resolved = $this->resolve($callback->resolve($this->container)); |
||
225 | } elseif ($callback instanceof Builder\PhpLiteral) { |
||
226 | $expression = $this->literalCache[\spl_object_id($callback)] ??= $callback->resolve($this)[0]; |
||
227 | $resolved = $expression instanceof Stmt\Expression ? $expression->expr : $expression; |
||
228 | } elseif (Services\ServiceLocator::class === $callback) { |
||
229 | $services = []; |
||
230 | |||
231 | foreach ($args as $name => $service) { |
||
232 | $services += $this->resolveServiceSubscriber($name, (string) $service); |
||
233 | } |
||
234 | $resolved = null === $this->builder ? new Services\ServiceLocator($services) : $this->builder->new('\\' . Services\ServiceLocator::class, [$services]); |
||
235 | } elseif (\is_string($callback)) { |
||
236 | if (\str_contains($callback, '%')) { |
||
237 | $callback = $this->container->parameter($callback); |
||
238 | } |
||
239 | |||
240 | if (\class_exists($callback)) { |
||
241 | return $this->resolveClass($callback, $args); |
||
242 | } |
||
243 | |||
244 | if (\is_callable($callback)) { |
||
245 | $resolved = $this->resolveCallable($callback, $args); |
||
246 | } |
||
247 | } elseif (\is_callable($callback) || \is_array($callback)) { |
||
248 | $resolved = $this->resolveCallable($callback, $args); |
||
249 | } |
||
250 | |||
251 | return $resolved ?? (null === $this->builder ? $callback : $this->builder->val($callback)); |
||
252 | } |
||
253 | |||
254 | /** |
||
255 | * Resolves callables and array like callables. |
||
256 | * |
||
257 | * @param callable|array<int,mixed> $callback |
||
258 | * @param array<int|string,mixed> $arguments |
||
259 | * |
||
260 | * @throws \ReflectionException if $callback is not a real callable |
||
261 | * |
||
262 | * @return mixed |
||
263 | */ |
||
264 | public function resolveCallable($callback, array $arguments = []) |
||
265 | { |
||
266 | if (\is_array($callback)) { |
||
267 | if ((2 === \count($callback) && \array_is_list($callback)) && \is_string($callback[1])) { |
||
268 | $callback[0] = $this->resolve($callback[0]); |
||
269 | |||
270 | if ($callback[0] instanceof Expr\BinaryOp\Coalesce) { |
||
271 | $class = self::getDefinitionClass($this->container->definition($callback[0]->left->dim->value)); |
||
272 | |||
273 | if (null !== $class) { |
||
274 | $type = [$class, $callback[1]]; |
||
275 | } |
||
276 | } elseif ($callback[0] instanceof Expr\New_) { |
||
277 | $type = [(string) $callback[0]->class, $callback[1]]; |
||
278 | } |
||
279 | |||
280 | if (isset($type) || \is_callable($callback)) { |
||
281 | goto create_callable; |
||
282 | } |
||
283 | } |
||
284 | |||
285 | $callback = $this->resolveArguments($callback); |
||
286 | |||
287 | return null === $this->builder ? $callback : $this->builder->val($callback); |
||
288 | } |
||
289 | |||
290 | create_callable: |
||
291 | $args = $this->autowireArguments($ref = Callback::toReflection($type ?? $callback), $arguments); |
||
292 | |||
293 | if ($ref instanceof \ReflectionFunction) { |
||
294 | return null === $this->builder ? $ref->invokeArgs($args) : $this->builder->funcCall($callback, $args); |
||
295 | } |
||
296 | |||
297 | if ($ref->isStatic()) { |
||
298 | $className = \is_array($callback) ? $callback[0] : $ref->getDeclaringClass()->getName(); |
||
299 | |||
300 | return null === $this->builder ? $ref->invokeArgs(null, $args) : $this->builder->staticCall($className, $ref->getName(), $args); |
||
301 | } |
||
302 | |||
303 | return null === $this->builder ? $callback(...$args) : $this->builder->methodCall($callback[0], $ref->getName(), $args); |
||
304 | } |
||
305 | |||
306 | /** |
||
307 | * @param array<int|string,mixed> $args |
||
308 | * |
||
309 | * @throws ContainerResolutionException|\ReflectionException if class string unresolvable |
||
310 | */ |
||
311 | public function resolveClass(string $class, array $args = []): object |
||
336 | } |
||
337 | |||
338 | /** |
||
339 | * @param array<int|string,mixed> $arguments |
||
340 | * |
||
341 | * @return array<int|string,mixed> |
||
342 | */ |
||
343 | public function resolveArguments(array $arguments = []): array |
||
370 | } |
||
371 | |||
372 | /** |
||
373 | * Resolves service by type. |
||
374 | * |
||
375 | * @param string $id A class or an interface name |
||
376 | * |
||
377 | * @return mixed |
||
378 | */ |
||
379 | public function get(string $id, bool $single = false) |
||
404 | } |
||
405 | |||
406 | /** |
||
407 | * Gets the PHP's parser builder. |
||
408 | */ |
||
409 | public function getBuilder(): ?BuilderFactory |
||
412 | } |
||
413 | |||
414 | /** |
||
415 | * @return mixed |
||
416 | */ |
||
417 | public function resolveReference(string $reference) |
||
418 | { |
||
419 | $invalidBehavior = $this->container::EXCEPTION_ON_MULTIPLE_SERVICE; |
||
420 | |||
421 | if ('?' === $reference[0]) { |
||
422 | $invalidBehavior = $this->container::NULL_ON_INVALID_SERVICE; |
||
423 | $reference = \substr($reference, 1); |
||
424 | } |
||
425 | |||
426 | if (1 === \preg_match('/\[(.*?)?\]$/', $reference, $matches, \PREG_UNMATCHED_AS_NULL)) { |
||
427 | $reference = \str_replace($matches[0], '', $reference); |
||
428 | $autowired = $this->container->typed($reference, true); |
||
429 | |||
430 | if (\is_numeric($k = $matches[1] ?? null) && isset($autowired[$k])) { |
||
431 | return $this->container->get($autowired[$k], $invalidBehavior); |
||
432 | } |
||
433 | |||
434 | if (!empty($autowired)) { |
||
435 | return \array_map([$this->container, 'get'], $autowired); |
||
436 | } |
||
437 | |||
438 | if (null === $service = $this->container->get($reference, $invalidBehavior)) { |
||
439 | return []; |
||
440 | } |
||
441 | |||
442 | return [$service]; |
||
443 | } |
||
444 | |||
445 | return $this->container->get($reference, $invalidBehavior); |
||
446 | } |
||
447 | |||
448 | /** |
||
449 | * Resolves services for ServiceLocator. |
||
450 | * |
||
451 | * @param int|string $id |
||
452 | * |
||
453 | * @return (\Closure|array|mixed|null)[] |
||
454 | */ |
||
455 | public function resolveServiceSubscriber($id, string $value): array |
||
456 | { |
||
457 | $service = fn () => $this->resolveReference($value); |
||
458 | |||
459 | if (null !== $this->builder) { |
||
460 | $type = \rtrim(\ltrim($value, '?'), '[]'); |
||
461 | |||
462 | if ('[]' === \substr($value, -2)) { |
||
463 | $returnType = 'array'; |
||
464 | } elseif ($this->container->has($type) && ($def = $this->container->definition($type)) instanceof Definitions\TypedDefinitionInterface) { |
||
465 | $returnType = $def->getTypes()[0] ?? ( |
||
466 | \class_exists($type) || \interface_exists($type) |
||
467 | ? $type |
||
468 | : (!\is_int($id) && (\class_exists($id) || \interface_exists($id)) ? $id : null) |
||
469 | ); |
||
470 | } elseif (\class_exists($type) || \interface_exists($type)) { |
||
471 | $returnType = $type; |
||
472 | } |
||
473 | |||
474 | $service = new Expr\ArrowFunction(['expr' => $this->builder->val($service()), 'returnType' => $returnType ?? null]); |
||
475 | } |
||
476 | |||
477 | return [\is_int($id) ? ($type ?? \rtrim(\ltrim($value, '?'), '[]')) : $id => $service]; |
||
478 | } |
||
479 | |||
480 | /** |
||
481 | * Resolves missing argument using autowiring. |
||
482 | * |
||
483 | * @param array<int|string,mixed> $providedParameters |
||
484 | * @param array<int,string> $types |
||
485 | * |
||
486 | * @throws ContainerResolutionException |
||
487 | * |
||
488 | * @return mixed |
||
489 | */ |
||
490 | public function autowireArgument(\ReflectionParameter $parameter, array $types, array $providedParameters) |
||
491 | { |
||
492 | foreach ($types as $typeName) { |
||
493 | if (\PHP_MAJOR_VERSION >= 8 && $attributes = $parameter->getAttributes()) { |
||
494 | foreach ($attributes as $attribute) { |
||
495 | if (Attribute\Inject::class === $attribute->getName()) { |
||
496 | try { |
||
497 | return $attribute->newInstance()->resolve($this, $typeName); |
||
498 | } catch (NotFoundServiceException $e) { |
||
499 | // Ignore this exception ... |
||
500 | } |
||
501 | } |
||
502 | |||
503 | if (Attribute\Tagged::class === $attribute->getName()) { |
||
504 | return $this->resolveArguments($attribute->newInstance()->getValues($this->container)); |
||
505 | } |
||
506 | } |
||
507 | } |
||
508 | |||
509 | if (!Reflection::isBuiltinType($typeName)) { |
||
510 | try { |
||
511 | return $providedParameters[$typeName] ?? $this->get($typeName, !$parameter->isVariadic()); |
||
512 | } catch (NotFoundServiceException $e) { |
||
513 | // Ignore this exception ... |
||
514 | } catch (ContainerResolutionException $e) { |
||
515 | $errorException = new ContainerResolutionException(\sprintf("{$e->getMessage()} (needed by %s)", Reflection::toString($parameter))); |
||
516 | } |
||
517 | |||
518 | if ( |
||
519 | ServiceProviderInterface::class === $typeName && |
||
520 | null !== $class = $parameter->getDeclaringClass() |
||
521 | ) { |
||
522 | if (!$class->implementsInterface(ServiceSubscriberInterface::class)) { |
||
523 | throw new ContainerResolutionException(\sprintf( |
||
524 | 'Service of type %s needs parent class %s to implement %s.', |
||
525 | $typeName, |
||
526 | $class->getName(), |
||
527 | ServiceSubscriberInterface::class |
||
528 | )); |
||
529 | } |
||
530 | |||
531 | return $this->get($class->getName()); |
||
532 | } |
||
533 | } |
||
534 | |||
535 | if ( |
||
536 | ($method = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod |
||
537 | && \preg_match('#@param[ \t]+([\w\\\\]+)(?:\[\])?[ \t]+\$' . $parameter->name . '#', (string) $method->getDocComment(), $m) |
||
538 | && ($itemType = Reflection::expandClassName($m[1], $method->getDeclaringClass())) |
||
539 | && (\class_exists($itemType) || \interface_exists($itemType)) |
||
540 | ) { |
||
541 | try { |
||
542 | if (\in_array($typeName, ['array', 'iterable'], true)) { |
||
543 | return $this->get($itemType); |
||
544 | } |
||
545 | |||
546 | if ('object' === $typeName || \is_subclass_of($itemType, $typeName)) { |
||
547 | return $this->get($itemType, true); |
||
548 | } |
||
549 | } catch (NotFoundServiceException $e) { |
||
550 | // Ignore this exception ... |
||
551 | } |
||
552 | } |
||
553 | |||
554 | if (isset($errorException)) { |
||
555 | throw $errorException; |
||
556 | } |
||
557 | } |
||
558 | |||
559 | return null; |
||
560 | } |
||
561 | |||
562 | /** |
||
563 | * Returns an associated type to the given parameter if available. |
||
564 | * |
||
565 | * @param \ReflectionParameter|\ReflectionFunctionAbstract $reflection |
||
566 | * |
||
567 | * @return array<int,string> |
||
568 | */ |
||
569 | public static function getTypes(\Reflector $reflection): array |
||
604 | } |
||
605 | |||
606 | private static function getDefinitionClass(Definitions\DefinitionInterface $def): ?string |
||
607 | { |
||
608 | if (!\is_string($class = $def->getEntity())) { |
||
609 | return null; |
||
610 | } |
||
611 | |||
612 | if (!\class_exists($class)) { |
||
613 | if ($def instanceof Definitions\TypedDefinitionInterface) { |
||
614 | foreach ($def->getTypes() as $typed) { |
||
615 | if (\class_exists($typed)) { |
||
616 | return $typed; |
||
617 | } |
||
618 | } |
||
619 | } |
||
620 | |||
621 | return null; |
||
622 | } |
||
623 | |||
624 | return $class; |
||
625 | } |
||
626 | |||
627 | /** |
||
628 | * Get the parameter's allowed null else error. |
||
629 | * |
||
630 | * @throws \ReflectionException|ContainerResolutionException |
||
631 | * |
||
632 | * @return void|null |
||
633 | */ |
||
634 | private static function getParameterDefaultValue(\ReflectionParameter $parameter, array $types) |
||
655 | } |
||
656 | } |
||
657 |
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