Total Complexity | 183 |
Total Lines | 607 |
Duplicated Lines | 0 % |
Changes | 15 | ||
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) |
||
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 |
||
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 |
||
131 | { |
||
132 | $resolvedParameters = []; |
||
133 | $nullValuesFound = 0; |
||
134 | $args = $this->resolveArguments($args); // Resolves provided arguments. |
||
135 | |||
136 | foreach ($function->getParameters() as $offset => $parameter) { |
||
137 | $position = 0 === $nullValuesFound ? $offset : $parameter->name; |
||
138 | $resolved = $args[$offset] ?? $args[$parameter->name] ?? null; |
||
139 | $types = self::getTypes($parameter) ?: ['null']; |
||
140 | |||
141 | if (\PHP_VERSION_ID >= 80100 && (\count($types) >= 1 && \is_subclass_of($enumType = $types[0], \BackedEnum::class))) { |
||
|
|||
142 | if (null === $resolved = ($resolved ?? $args[$enumType] ?? null)) { |
||
143 | throw new ContainerResolutionException(\sprintf('Missing value for enum parameter %s.', Reflection::toString($parameter))); |
||
144 | } |
||
145 | |||
146 | try { |
||
147 | $resolvedParameters[$position] = $enumType::from($resolved); |
||
148 | } catch (\ValueError $e) { |
||
149 | throw new ContainerResolutionException(\sprintf('The "%s" value could not be resolved for enum parameter %s.', $resolved, Reflection::toString($parameter)), 0, $e); |
||
150 | } |
||
151 | continue; |
||
152 | } |
||
153 | |||
154 | if (null === ($resolved = $resolved ?? $this->autowireArgument($parameter, $types, $args))) { |
||
155 | if ($parameter->isDefaultValueAvailable()) { |
||
156 | if (\PHP_MAJOR_VERSION < 8) { |
||
157 | $resolvedParameters[$position] = Reflection::getParameterDefaultValue($parameter); |
||
158 | } else { |
||
159 | ++$nullValuesFound; |
||
160 | } |
||
161 | } elseif (!$parameter->isVariadic()) { |
||
162 | $resolvedParameters[$position] = self::getParameterDefaultValue($parameter, $types); |
||
163 | } |
||
164 | |||
165 | continue; |
||
166 | } |
||
167 | |||
168 | if ($parameter->isVariadic() && \is_array($resolved)) { |
||
169 | $resolvedParameters = \array_merge($resolvedParameters, $resolved); |
||
170 | continue; |
||
171 | } |
||
172 | |||
173 | $resolvedParameters[$position] = $resolved; |
||
174 | } |
||
175 | |||
176 | return $resolvedParameters; |
||
177 | } |
||
178 | |||
179 | /** |
||
180 | * Resolve a service definition, class string, invocable object or callable |
||
181 | * using autowiring. |
||
182 | * |
||
183 | * @param string|callable|object $callback |
||
184 | * @param array<int|string,mixed> $args |
||
185 | * |
||
186 | * @throws ContainerResolutionException|\ReflectionException if unresolvable |
||
187 | * |
||
188 | * @return mixed |
||
189 | */ |
||
190 | public function resolve($callback, array $args = []) |
||
191 | { |
||
192 | if ($callback instanceof Definitions\Statement) { |
||
193 | $resolved = $this->resolve($callback->getValue(), $callback->getArguments() + $args); |
||
194 | |||
195 | if ($callback->isClosureWrappable()) { |
||
196 | $resolved = null === $this->builder ? fn () => $resolved : new Expr\ArrowFunction(['expr' => $resolved]); |
||
197 | } |
||
198 | } elseif ($callback instanceof Definitions\Reference) { |
||
199 | $resolved = $this->resolveReference((string) $callback); |
||
200 | |||
201 | if (\is_callable($resolved) || (\is_array($resolved) && 2 === \count($resolved, \COUNT_RECURSIVE))) { |
||
202 | $resolved = $this->resolveCallable($resolved, $args); |
||
203 | } else { |
||
204 | $callback = $resolved; |
||
205 | } |
||
206 | } elseif ($callback instanceof Definitions\ValueDefinition) { |
||
207 | $resolved = $callback->getEntity(); |
||
208 | } elseif ($callback instanceof Definitions\TaggedLocator) { |
||
209 | $resolved = $this->resolve($callback->resolve($this->container)); |
||
210 | } elseif ($callback instanceof Builder\PhpLiteral) { |
||
211 | $expression = $this->literalCache[\spl_object_id($callback)] ??= $callback->resolve($this)[0]; |
||
212 | $resolved = $expression instanceof Stmt\Expression ? $expression->expr : $expression; |
||
213 | } elseif (Services\ServiceLocator::class === $callback) { |
||
214 | $services = []; |
||
215 | |||
216 | foreach ($args as $name => $service) { |
||
217 | $services += $this->resolveServiceSubscriber($name, (string) $service); |
||
218 | } |
||
219 | $resolved = null === $this->builder ? new Services\ServiceLocator($services) : $this->builder->new('\\' . Services\ServiceLocator::class, [$services]); |
||
220 | } elseif (\is_string($callback)) { |
||
221 | if (\str_contains($callback, '%')) { |
||
222 | $callback = $this->container->parameter($callback); |
||
223 | } |
||
224 | |||
225 | if (\class_exists($callback)) { |
||
226 | return $this->resolveClass($callback, $args); |
||
227 | } |
||
228 | |||
229 | if (\is_callable($callback)) { |
||
230 | $resolved = $this->resolveCallable($callback, $args); |
||
231 | } |
||
232 | } elseif (\is_callable($callback) || \is_array($callback)) { |
||
233 | $resolved = $this->resolveCallable($callback, $args); |
||
234 | } |
||
235 | |||
236 | return $resolved ?? (null === $this->builder ? $callback : $this->builder->val($callback)); |
||
237 | } |
||
238 | |||
239 | /** |
||
240 | * Resolves callables and array like callables. |
||
241 | * |
||
242 | * @param callable|array<int,mixed> $callback |
||
243 | * @param array<int|string,mixed> $arguments |
||
244 | * |
||
245 | * @throws \ReflectionException if $callback is not a real callable |
||
246 | * |
||
247 | * @return mixed |
||
248 | */ |
||
249 | public function resolveCallable($callback, array $arguments = []) |
||
250 | { |
||
251 | if (\is_array($callback)) { |
||
252 | if (2 === \count($callback, \COUNT_RECURSIVE) && \is_string($callback[1])) { |
||
253 | $callback[0] = $this->resolve($callback[0]); |
||
254 | |||
255 | if ($callback[0] instanceof Expr\BinaryOp\Coalesce) { |
||
256 | $class = self::getDefinitionClass($this->container->definition($callback[0]->left->dim->value)); |
||
257 | |||
258 | if (null !== $class) { |
||
259 | $type = [$class, $callback[1]]; |
||
260 | } |
||
261 | } elseif ($callback[0] instanceof Expr\New_) { |
||
262 | $type = [(string) $callback[0]->class, $callback[1]]; |
||
263 | } |
||
264 | |||
265 | if (isset($type) || \is_callable($callback)) { |
||
266 | goto create_callable; |
||
267 | } |
||
268 | } |
||
269 | |||
270 | $callback = $this->resolveArguments($callback); |
||
271 | |||
272 | return null === $this->builder ? $callback : $this->builder->val($callback); |
||
273 | } |
||
274 | |||
275 | create_callable: |
||
276 | $args = $this->autowireArguments($ref = Callback::toReflection($type ?? $callback), $arguments); |
||
277 | |||
278 | if ($ref instanceof \ReflectionFunction) { |
||
279 | return null === $this->builder ? $ref->invokeArgs($args) : $this->builder->funcCall($callback, $args); |
||
280 | } |
||
281 | |||
282 | if ($ref->isStatic()) { |
||
283 | $className = \is_array($callback) ? $callback[0] : $ref->getDeclaringClass()->getName(); |
||
284 | |||
285 | return null === $this->builder ? $ref->invokeArgs(null, $args) : $this->builder->staticCall($className, $ref->getName(), $args); |
||
286 | } |
||
287 | |||
288 | return null === $this->builder ? $callback(...$args) : $this->builder->methodCall($callback[0], $ref->getName(), $args); |
||
289 | } |
||
290 | |||
291 | /** |
||
292 | * @param array<int|string,mixed> $args |
||
293 | * |
||
294 | * @throws ContainerResolutionException|\ReflectionException if class string unresolvable |
||
295 | */ |
||
296 | public function resolveClass(string $class, array $args = []): object |
||
297 | { |
||
298 | /** @var class-string $class */ |
||
299 | $reflection = new \ReflectionClass($class); |
||
300 | |||
301 | if ($reflection->isAbstract() || !$reflection->isInstantiable()) { |
||
302 | throw new ContainerResolutionException(\sprintf('Class %s is an abstract type or instantiable.', $class)); |
||
303 | } |
||
304 | |||
305 | if (null === $constructor = $reflection->getConstructor()) { |
||
306 | if (!empty($args)) { |
||
307 | throw new ContainerResolutionException(\sprintf('Unable to pass arguments, class "%s" has no constructor.', $class)); |
||
308 | } |
||
309 | |||
310 | $service = null === $this->builder ? $reflection->newInstanceWithoutConstructor() : $this->builder->new($class); |
||
311 | } else { |
||
312 | $args = $this->autowireArguments($constructor, $args); |
||
313 | $service = null === $this->builder ? $reflection->newInstanceArgs($args) : $this->builder->new($class, $args); |
||
314 | } |
||
315 | |||
316 | if ($reflection->implementsInterface(Injector\InjectableInterface::class)) { |
||
317 | return Injector\Injectable::getResolved($this, $service, $reflection); |
||
318 | } |
||
319 | |||
320 | return $service; |
||
321 | } |
||
322 | |||
323 | /** |
||
324 | * @param array<int|string,mixed> $arguments |
||
325 | * |
||
326 | * @return array<int|string,mixed> |
||
327 | */ |
||
328 | public function resolveArguments(array $arguments = []): array |
||
329 | { |
||
330 | foreach ($arguments as $key => $value) { |
||
331 | if ($value instanceof \stdClass) { |
||
332 | $resolved = null === $this->builder ? $value : new Expr\Cast\Object_($this->builder->val($this->resolveArguments((array) $value))); |
||
333 | } elseif (\is_array($value)) { |
||
334 | $resolved = $this->resolveArguments($value); |
||
335 | } elseif (\is_int($value)) { |
||
336 | $resolved = null === $this->builder ? $value : new Scalar\LNumber($value); |
||
337 | } elseif (\is_float($value)) { |
||
338 | $resolved = null === $this->builder ? (int) $value : new Scalar\DNumber($value); |
||
339 | } elseif (\is_numeric($value)) { |
||
340 | $resolved = null === $this->builder ? (int) $value : Scalar\LNumber::fromString($value); |
||
341 | } elseif (\is_string($value)) { |
||
342 | if (\str_contains($value, '%')) { |
||
343 | $value = $this->container->parameter($value); |
||
344 | } |
||
345 | |||
346 | $resolved = null === $this->builder ? $value : $this->builder->val($value); |
||
347 | } else { |
||
348 | $resolved = $this->resolve($value); |
||
349 | } |
||
350 | |||
351 | $arguments[$key] = $resolved; |
||
352 | } |
||
353 | |||
354 | return $arguments; |
||
355 | } |
||
356 | |||
357 | /** |
||
358 | * Resolves service by type. |
||
359 | * |
||
360 | * @param string $id A class or an interface name |
||
361 | * |
||
362 | * @return mixed |
||
363 | */ |
||
364 | public function get(string $id, bool $single = false) |
||
389 | } |
||
390 | |||
391 | /** |
||
392 | * Gets the PHP's parser builder. |
||
393 | */ |
||
394 | public function getBuilder(): ?BuilderFactory |
||
397 | } |
||
398 | |||
399 | /** |
||
400 | * @return mixed |
||
401 | */ |
||
402 | public function resolveReference(string $reference) |
||
403 | { |
||
404 | if ('?' === $reference[0]) { |
||
405 | $invalidBehavior = $this->container::EXCEPTION_ON_MULTIPLE_SERVICE; |
||
406 | $reference = \substr($reference, 1); |
||
407 | |||
408 | if ($arrayLike = \str_ends_with($reference, '[]')) { |
||
409 | $reference = \substr($reference, 0, -2); |
||
410 | $invalidBehavior = $this->container::IGNORE_MULTIPLE_SERVICE; |
||
411 | } |
||
412 | |||
413 | if ($this->container->has($reference) || $this->container->typed($reference)) { |
||
414 | $service = $this->container->get($reference, $invalidBehavior); |
||
415 | |||
416 | return !$arrayLike ? $service : (\is_array($service) ? $service : [$service]); |
||
417 | } |
||
418 | |||
419 | return $arrayLike ? [] : null; |
||
420 | } |
||
421 | |||
422 | if ('[]' === \substr($reference, -2)) { |
||
423 | $service = $this->container->get(\substr($reference, 0, -2), $this->container::IGNORE_MULTIPLE_SERVICE); |
||
424 | |||
425 | return \is_array($service) ? $service : [$service]; |
||
426 | } |
||
427 | |||
428 | return $this->container->get($reference); |
||
429 | } |
||
430 | |||
431 | /** |
||
432 | * Resolves services for ServiceLocator. |
||
433 | * |
||
434 | * @param int|string $id |
||
435 | * |
||
436 | * @return (\Closure|array|mixed|null)[] |
||
437 | */ |
||
438 | public function resolveServiceSubscriber($id, string $value): array |
||
439 | { |
||
440 | $service = fn () => $this->resolveReference($value); |
||
441 | |||
442 | if (null !== $this->builder) { |
||
443 | $type = \rtrim(\ltrim($value, '?'), '[]'); |
||
444 | |||
445 | if ('[]' === \substr($value, -2)) { |
||
446 | $returnType = 'array'; |
||
447 | } elseif ($this->container->has($type) && ($def = $this->container->definition($type)) instanceof Definitions\TypedDefinitionInterface) { |
||
448 | $returnType = $def->getTypes()[0] ?? ( |
||
449 | \class_exists($type) || \interface_exists($type) |
||
450 | ? $type |
||
451 | : (!\is_int($id) && (\class_exists($id) || \interface_exists($id)) ? $id : null) |
||
452 | ); |
||
453 | } elseif (\class_exists($type) || \interface_exists($type)) { |
||
454 | $returnType = $type; |
||
455 | } |
||
456 | |||
457 | $service = new Expr\ArrowFunction(['expr' => $this->builder->val($service()), 'returnType' => $returnType ?? null]); |
||
458 | } |
||
459 | |||
460 | return [\is_int($id) ? ($type ?? \rtrim(\ltrim($value, '?'), '[]')) : $id => $service]; |
||
461 | } |
||
462 | |||
463 | /** |
||
464 | * Resolves missing argument using autowiring. |
||
465 | * |
||
466 | * @param array<int|string,mixed> $providedParameters |
||
467 | * @param array<int,string> $types |
||
468 | * |
||
469 | * @throws ContainerResolutionException |
||
470 | * |
||
471 | * @return mixed |
||
472 | */ |
||
473 | public function autowireArgument(\ReflectionParameter $parameter, array $types, array $providedParameters) |
||
474 | { |
||
475 | foreach ($types as $typeName) { |
||
476 | if (!Reflection::isBuiltinType($typeName)) { |
||
477 | try { |
||
478 | return $providedParameters[$typeName] ?? $this->get($typeName, !$parameter->isVariadic()); |
||
479 | } catch (NotFoundServiceException $e) { |
||
480 | // Ignore this exception ... |
||
481 | } catch (ContainerResolutionException $e) { |
||
482 | $errorException = new ContainerResolutionException(\sprintf("{$e->getMessage()} (needed by %s)", Reflection::toString($parameter))); |
||
483 | } |
||
484 | |||
485 | if ( |
||
486 | ServiceProviderInterface::class === $typeName && |
||
487 | null !== $class = $parameter->getDeclaringClass() |
||
488 | ) { |
||
489 | if (!$class->implementsInterface(ServiceSubscriberInterface::class)) { |
||
490 | throw new ContainerResolutionException(\sprintf( |
||
491 | 'Service of type %s needs parent class %s to implement %s.', |
||
492 | $typeName, |
||
493 | $class->getName(), |
||
494 | ServiceSubscriberInterface::class |
||
495 | )); |
||
496 | } |
||
497 | |||
498 | return $this->get($class->getName()); |
||
499 | } |
||
500 | } |
||
501 | |||
502 | if (\PHP_MAJOR_VERSION >= 8 && $attributes = $parameter->getAttributes()) { |
||
503 | foreach ($attributes as $attribute) { |
||
504 | if (Attribute\Inject::class === $attribute->getName()) { |
||
505 | try { |
||
506 | return $this->resolveReference($attribute->getArguments()[0] ?? $typeName); |
||
507 | } catch (NotFoundServiceException $e) { |
||
508 | // Ignore this exception ... |
||
509 | } |
||
510 | } |
||
511 | |||
512 | if (Attribute\Tagged::class === $attribute->getName()) { |
||
513 | return $this->resolveArguments($attribute->newInstance()->getValues($this->container)); |
||
514 | } |
||
515 | } |
||
516 | } |
||
517 | |||
518 | if ( |
||
519 | ($method = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod |
||
520 | && \preg_match('#@param[ \t]+([\w\\\\]+)(?:\[\])?[ \t]+\$' . $parameter->name . '#', (string) $method->getDocComment(), $m) |
||
521 | && ($itemType = Reflection::expandClassName($m[1], $method->getDeclaringClass())) |
||
522 | && (\class_exists($itemType) || \interface_exists($itemType)) |
||
523 | ) { |
||
524 | try { |
||
525 | if (\in_array($typeName, ['array', 'iterable'], true)) { |
||
526 | return $this->get($itemType); |
||
527 | } |
||
528 | |||
529 | if ('object' === $typeName || \is_subclass_of($itemType, $typeName)) { |
||
530 | return $this->get($itemType, true); |
||
531 | } |
||
532 | } catch (NotFoundServiceException $e) { |
||
533 | // Ignore this exception ... |
||
534 | } |
||
535 | } |
||
536 | |||
537 | if (isset($errorException)) { |
||
538 | throw $errorException; |
||
539 | } |
||
540 | } |
||
541 | |||
542 | return null; |
||
543 | } |
||
544 | |||
545 | /** |
||
546 | * Returns an associated type to the given parameter if available. |
||
547 | * |
||
548 | * @param \ReflectionParameter|\ReflectionFunctionAbstract $reflection |
||
549 | * |
||
550 | * @return array<int,string> |
||
551 | */ |
||
552 | public static function getTypes(\Reflector $reflection): array |
||
587 | } |
||
588 | |||
589 | private static function getDefinitionClass(Definitions\DefinitionInterface $def): ?string |
||
590 | { |
||
591 | if (!\is_string($class = $def->getEntity())) { |
||
592 | return null; |
||
593 | } |
||
594 | |||
595 | if (!\class_exists($class)) { |
||
596 | if ($def instanceof Definitions\TypedDefinitionInterface) { |
||
597 | foreach ($def->getTypes() as $typed) { |
||
598 | if (\class_exists($typed)) { |
||
599 | return $typed; |
||
600 | } |
||
601 | } |
||
602 | } |
||
603 | |||
604 | return null; |
||
605 | } |
||
606 | |||
607 | return $class; |
||
608 | } |
||
609 | |||
610 | /** |
||
611 | * Get the parameter's allowed null else error. |
||
612 | * |
||
613 | * @throws \ReflectionException|ContainerResolutionException |
||
614 | * |
||
615 | * @return null|void |
||
616 | */ |
||
617 | private static function getParameterDefaultValue(\ReflectionParameter $parameter, array $types) |
||
638 | } |
||
639 | } |
||
640 |
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