| Total Complexity | 184 |
| Total Lines | 611 |
| Duplicated Lines | 0 % |
| Changes | 2 | ||
| Bugs | 0 | 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 | |||
| 35 | private ?BuilderFactory $builder; |
||
| 36 | |||
| 37 | private bool $strict = true; |
||
| 38 | |||
| 39 | /** @var array<string,\PhpParser\Node> */ |
||
| 40 | private array $literalCache = []; |
||
| 41 | |||
| 42 | public function __construct(AbstractContainer $container, BuilderFactory $builder = null) |
||
| 43 | { |
||
| 44 | $this->builder = $builder; |
||
| 45 | $this->container = $container; |
||
| 46 | } |
||
| 47 | |||
| 48 | /** |
||
| 49 | * If true, exception will be thrown on resolvable services with are not typed. |
||
| 50 | */ |
||
| 51 | public function setStrictAutowiring(bool $boolean = true): void |
||
| 52 | { |
||
| 53 | $this->strict = $boolean; |
||
| 54 | } |
||
| 55 | |||
| 56 | /** |
||
| 57 | * The method name generated for a service definition. |
||
| 58 | */ |
||
| 59 | public function createMethod(string $id): string |
||
| 60 | { |
||
| 61 | return 'get' . \str_replace(['.', '_', '\\'], '', \ucwords($id, '._')); |
||
| 62 | } |
||
| 63 | |||
| 64 | /** |
||
| 65 | * @param mixed $definition |
||
| 66 | */ |
||
| 67 | public static function autowireService($definition, bool $allTypes = false, AbstractContainer $container = null): array |
||
| 68 | { |
||
| 69 | $types = $autowired = []; |
||
| 70 | |||
| 71 | if (\is_callable($definition)) { |
||
| 72 | $definition = \Closure::fromCallable($definition); |
||
| 73 | } |
||
| 74 | |||
| 75 | if ($definition instanceof \Closure) { |
||
| 76 | $definition = Callback::unwrap($definition); |
||
| 77 | $types = self::getTypes(\is_array($definition) ? new \ReflectionMethod($definition[0], $definition[1]) : new \ReflectionFunction($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 | $types = self::getTypes(new \ReflectionMethod($def instanceof Definitions\DefinitionInterface ? $def->getEntity() : $def, $definition[1])); |
||
| 94 | |||
| 95 | goto resolve_types; |
||
| 96 | } |
||
| 97 | } |
||
| 98 | |||
| 99 | return $allTypes ? ['array'] : []; |
||
| 100 | } |
||
| 101 | |||
| 102 | if (\is_callable($definition)) { |
||
| 103 | $types = self::getTypes(Callback::toReflection($definition)); |
||
| 104 | } elseif (\is_string($definition)) { |
||
| 105 | if (!(\class_exists($definition) || \interface_exists($definition))) { |
||
| 106 | return $allTypes ? ['string'] : $types; |
||
| 107 | } |
||
| 108 | |||
| 109 | $types[] = $definition; |
||
| 110 | } elseif (\is_object($definition)) { |
||
| 111 | if ($definition instanceof \stdClass) { |
||
| 112 | return $allTypes ? ['object'] : $types; |
||
| 113 | } |
||
| 114 | |||
| 115 | $types[] = \get_class($definition); |
||
| 116 | } elseif (\is_array($definition)) { |
||
| 117 | if (null !== $container && 2 === \count($definition, \COUNT_RECURSIVE)) { |
||
| 118 | if ($definition[0] instanceof Definitions\Reference) { |
||
| 119 | $types = self::getTypes(new \ReflectionMethod($container->definition((string) $definition[0])->getEntity(), $definition[1])); |
||
| 120 | } elseif ($definition[0] instanceof Expr\BinaryOp\Coalesce) { |
||
| 121 | $types = self::getTypes(new \ReflectionMethod($container->definition($definition[0]->left->dim->value)->getEntity(), $definition[1])); |
||
| 122 | } |
||
| 123 | } else { |
||
| 124 | return $allTypes ? ['array'] : []; |
||
| 125 | } |
||
| 126 | } |
||
| 127 | |||
| 128 | resolve_types: |
||
| 129 | foreach (($types ?? []) as $type) { |
||
| 130 | $autowired[] = $type; |
||
| 131 | |||
| 132 | foreach (\class_implements($type) ?: [] as $interface) { |
||
| 133 | $autowired[] = $interface; |
||
| 134 | } |
||
| 135 | |||
| 136 | foreach (\class_parents($type) ?: [] as $parent) { |
||
| 137 | $autowired[] = $parent; |
||
| 138 | } |
||
| 139 | } |
||
| 140 | |||
| 141 | return $autowired; |
||
| 142 | } |
||
| 143 | |||
| 144 | /** |
||
| 145 | * Resolves arguments for callable. |
||
| 146 | * |
||
| 147 | * @param array<int|string,mixed> $args |
||
| 148 | * |
||
| 149 | * @return array<int,mixed> |
||
| 150 | */ |
||
| 151 | public function autowireArguments(\ReflectionFunctionAbstract $function, array $args = []): array |
||
| 195 | } |
||
| 196 | |||
| 197 | /** |
||
| 198 | * Resolve a service definition, class string, invocable object or callable |
||
| 199 | * using autowiring. |
||
| 200 | * |
||
| 201 | * @param string|callable|object $callback |
||
| 202 | * @param array<int|string,mixed> $args |
||
| 203 | * |
||
| 204 | * @throws ContainerResolutionException|\ReflectionException if unresolvable |
||
| 205 | * |
||
| 206 | * @return mixed |
||
| 207 | */ |
||
| 208 | public function resolve($callback, array $args = []) |
||
| 209 | { |
||
| 210 | if ($callback instanceof Definitions\Statement) { |
||
| 211 | if (Services\ServiceLocator::class == ($value = $callback->getValue())) { |
||
| 212 | $services = []; |
||
| 213 | |||
| 214 | foreach (($callback->getArguments() ?: $args) as $name => $service) { |
||
| 215 | $services += $this->resolveServiceSubscriber($name, (string) $service); |
||
| 216 | } |
||
| 217 | |||
| 218 | $resolved = null === $this->builder ? new Services\ServiceLocator($services) : $this->builder->new('\\' . Services\ServiceLocator::class, [$services]); |
||
| 219 | } else { |
||
| 220 | $resolved = $this->resolve($value, $callback->getArguments() ?: $args); |
||
| 221 | |||
| 222 | if ($callback->isClosureWrappable()) { |
||
| 223 | $resolved = null === $this->builder ? fn () => $resolved : new Expr\ArrowFunction(['expr' => $resolved]); |
||
| 224 | } |
||
| 225 | } |
||
| 226 | } elseif ($callback instanceof Definitions\Reference) { |
||
| 227 | $resolved = $this->resolveReference((string) $callback); |
||
| 228 | |||
| 229 | if (\is_callable($resolved) || (\is_array($resolved) && 2 === \count($resolved, \COUNT_RECURSIVE))) { |
||
| 230 | $resolved = $this->resolveCallable($resolved, $args); |
||
| 231 | } |
||
| 232 | } elseif ($callback instanceof Definitions\ValueDefinition) { |
||
| 233 | $resolved = $callback->getEntity(); |
||
| 234 | } elseif ($callback instanceof Definitions\TaggedLocator) { |
||
| 235 | $resolved = $this->resolve($callback->resolve($this->container)); |
||
| 236 | } elseif ($callback instanceof Builder\PhpLiteral) { |
||
| 237 | $expression = $this->literalCache[\spl_object_id($callback)] ??= $callback->resolve($this)[0]; |
||
| 238 | $resolved = $expression instanceof Stmt\Expression ? $expression->expr : $expression; |
||
| 239 | } elseif (\is_string($callback)) { |
||
| 240 | if (\str_contains($callback, '%')) { |
||
| 241 | $callback = $this->container->parameter($callback); |
||
| 242 | } |
||
| 243 | |||
| 244 | if (\class_exists($callback)) { |
||
| 245 | return $this->resolveClass($callback, $args); |
||
| 246 | } |
||
| 247 | |||
| 248 | if (\is_callable($callback)) { |
||
| 249 | $resolved = $this->resolveCallable($callback, $args); |
||
| 250 | } |
||
| 251 | } elseif (\is_callable($callback) || \is_array($callback)) { |
||
| 252 | $resolved = $this->resolveCallable($callback, $args); |
||
| 253 | } |
||
| 254 | |||
| 255 | return $resolved ?? (null === $this->builder ? $callback : $this->builder->val($callback)); |
||
| 256 | } |
||
| 257 | |||
| 258 | /** |
||
| 259 | * Resolves callables and array like callables. |
||
| 260 | * |
||
| 261 | * @param callable|array<int,mixed> $callback |
||
| 262 | * @param array<int|string,mixed> $arguments |
||
| 263 | * |
||
| 264 | * @throws \ReflectionException if $callback is not a real callable |
||
| 265 | * |
||
| 266 | * @return mixed |
||
| 267 | */ |
||
| 268 | public function resolveCallable($callback, array $arguments = []) |
||
| 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) |
||
| 380 | { |
||
| 381 | if (\is_subclass_of($id, ServiceSubscriberInterface::class)) { |
||
| 382 | static $services = []; |
||
| 383 | |||
| 384 | foreach ($id::getSubscribedServices() as $name => $service) { |
||
| 385 | $services += $this->resolveServiceSubscriber($name, $service); |
||
| 386 | } |
||
| 387 | |||
| 388 | if (null === $builder = $this->builder) { |
||
| 389 | return new Services\ServiceLocator($services); |
||
| 390 | } |
||
| 391 | |||
| 392 | return $builder->new('\\' . Services\ServiceLocator::class, [$services]); |
||
| 393 | } |
||
| 394 | |||
| 395 | if (!$this->strict) { |
||
| 396 | return $this->container->get($id, $single ? $this->container::EXCEPTION_ON_MULTIPLE_SERVICE : $this->container::IGNORE_MULTIPLE_SERVICE); |
||
| 397 | } |
||
| 398 | |||
| 399 | if ($this->container->typed($id)) { |
||
| 400 | return $this->container->autowired($id, $single); |
||
| 401 | } |
||
| 402 | |||
| 403 | throw new NotFoundServiceException(\sprintf('Service of type "%s" not found. Check class name because it cannot be found.', $id)); |
||
| 404 | } |
||
| 405 | |||
| 406 | /** |
||
| 407 | * Gets the PHP's parser builder. |
||
| 408 | */ |
||
| 409 | public function getBuilder(): ?BuilderFactory |
||
| 410 | { |
||
| 411 | return $this->builder; |
||
| 412 | } |
||
| 413 | |||
| 414 | /** |
||
| 415 | * @return mixed |
||
| 416 | */ |
||
| 417 | private function resolveReference(string $reference) |
||
| 418 | { |
||
| 419 | if ('?' === $reference[0]) { |
||
| 420 | $invalidBehavior = $this->container::EXCEPTION_ON_MULTIPLE_SERVICE; |
||
| 421 | $reference = \substr($reference, 1); |
||
| 422 | |||
| 423 | if ($arrayLike = \str_ends_with('[]', $reference)) { |
||
| 424 | $reference = \substr($reference, 0, -2); |
||
| 425 | $invalidBehavior = $this->container::IGNORE_MULTIPLE_SERVICE; |
||
| 426 | } |
||
| 427 | |||
| 428 | if ($this->container->has($reference) || $this->container->typed($reference)) { |
||
| 429 | return $this->container->get($reference, $invalidBehavior); |
||
| 430 | } |
||
| 431 | |||
| 432 | return $arrayLike ? [] : null; |
||
| 433 | } |
||
| 434 | |||
| 435 | if ('[]' === \substr($reference, -2)) { |
||
| 436 | return $this->container->get(\substr($reference, 0, -2), $this->container::IGNORE_MULTIPLE_SERVICE); |
||
| 437 | } |
||
| 438 | |||
| 439 | return $this->container->get($reference); |
||
| 440 | } |
||
| 441 | |||
| 442 | /** |
||
| 443 | * Resolves services for ServiceLocator. |
||
| 444 | * |
||
| 445 | * @param int|string $id |
||
| 446 | * |
||
| 447 | * @return (\Closure|array|mixed|null)[] |
||
| 448 | */ |
||
| 449 | private function resolveServiceSubscriber($id, string $value): array |
||
| 450 | { |
||
| 451 | if ('?' === $value[0]) { |
||
| 452 | $arrayLike = \str_ends_with($value = \substr($value, 1), '[]'); |
||
| 453 | |||
| 454 | if (\is_int($id)) { |
||
| 455 | $id = $arrayLike ? \substr($value, 0, -2) : $value; |
||
| 456 | } |
||
| 457 | |||
| 458 | return ($this->container->has($id) || $this->container->typed($id)) ? $this->resolveServiceSubscriber($id, $value) : [$id => $arrayLike ? [] : null]; |
||
| 459 | } |
||
| 460 | |||
| 461 | $service = function () use ($value) { |
||
| 462 | if ('[]' === \substr($value, -2)) { |
||
| 463 | $service = $this->container->get(\substr($value, 0, -2), $this->container::IGNORE_MULTIPLE_SERVICE); |
||
| 464 | |||
| 465 | return \is_array($service) ? $service : [$service]; |
||
| 466 | } |
||
| 467 | |||
| 468 | return $this->container->get($value); |
||
| 469 | }; |
||
| 470 | |||
| 471 | if (null !== $this->builder) { |
||
| 472 | if ($this->container->has($value)) { |
||
| 473 | $returnType = $this->container->definition($value)->getTypes()[0] ?? (\class_exists($id) || \interface_exists($id) ? $id : null); |
||
| 474 | } elseif ('[]' !== \substr($value, -2)) { |
||
| 475 | $returnType = 'array'; |
||
| 476 | } |
||
| 477 | |||
| 478 | $service = new Expr\ArrowFunction(['expr' => $this->builder->val($service()), 'returnType' => $returnType ?? null]); |
||
| 479 | } |
||
| 480 | |||
| 481 | return [\is_int($id) ? \rtrim($value, '[]') : $id => $service]; |
||
| 482 | } |
||
| 483 | |||
| 484 | /** |
||
| 485 | * Resolves missing argument using autowiring. |
||
| 486 | * |
||
| 487 | * @param array<int|string,mixed> $providedParameters |
||
| 488 | * @param array<int,string> $types |
||
| 489 | * |
||
| 490 | * @throws ContainerResolutionException |
||
| 491 | * |
||
| 492 | * @return mixed |
||
| 493 | */ |
||
| 494 | private function autowireArgument(\ReflectionParameter $parameter, array $types, array $providedParameters) |
||
| 495 | { |
||
| 496 | foreach ($types as $typeName) { |
||
| 497 | if (!Reflection::isBuiltinType($typeName)) { |
||
| 498 | try { |
||
| 499 | return $providedParameters[$typeName] ?? $this->get($typeName, !$parameter->isVariadic()); |
||
| 500 | } catch (NotFoundServiceException $e) { |
||
| 501 | // Ignore this exception ... |
||
| 502 | } catch (ContainerResolutionException $e) { |
||
| 503 | $errorException = new ContainerResolutionException(\sprintf("{$e->getMessage()} (needed by %s)", Reflection::toString($parameter))); |
||
| 504 | } |
||
| 505 | |||
| 506 | if ( |
||
| 507 | ServiceProviderInterface::class === $typeName && |
||
| 508 | null !== $class = $parameter->getDeclaringClass() |
||
| 509 | ) { |
||
| 510 | if (!$class->implementsInterface(ServiceSubscriberInterface::class)) { |
||
| 511 | throw new ContainerResolutionException(\sprintf( |
||
| 512 | 'Service of type %s needs parent class %s to implement %s.', |
||
| 513 | $typeName, |
||
| 514 | $class->getName(), |
||
| 515 | ServiceSubscriberInterface::class |
||
| 516 | )); |
||
| 517 | } |
||
| 518 | |||
| 519 | return $this->get($class->getName()); |
||
| 520 | } |
||
| 521 | } |
||
| 522 | |||
| 523 | if (\PHP_MAJOR_VERSION >= 8 && $attributes = $parameter->getAttributes()) { |
||
| 524 | foreach ($attributes as $attribute) { |
||
| 525 | if (Attribute\Inject::class === $attribute->getName()) { |
||
| 526 | if (null === $attrName = $attribute->getArguments()[0] ?? null) { |
||
| 527 | throw new ContainerResolutionException(\sprintf('Using the Inject attribute on parameter %s requires a value to be set.', $parameter->getName())); |
||
| 528 | } |
||
| 529 | |||
| 530 | if ($arrayLike = \str_ends_with($attrName, '[]')) { |
||
| 531 | $attrName = \substr($attrName, 0, -2); |
||
| 532 | } |
||
| 533 | |||
| 534 | try { |
||
| 535 | return $this->get($attrName, !$arrayLike); |
||
| 536 | } catch (NotFoundServiceException $e) { |
||
| 537 | // Ignore this exception ... |
||
| 538 | } |
||
| 539 | } |
||
| 540 | } |
||
| 541 | } |
||
| 542 | |||
| 543 | if ( |
||
| 544 | ($method = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod |
||
| 545 | && \preg_match('#@param[ \t]+([\w\\\\]+)(?:\[\])?[ \t]+\$' . $parameter->name . '#', (string) $method->getDocComment(), $m) |
||
| 546 | && ($itemType = Reflection::expandClassName($m[1], $method->getDeclaringClass())) |
||
| 547 | && (\class_exists($itemType) || \interface_exists($itemType)) |
||
| 548 | ) { |
||
| 549 | try { |
||
| 550 | if (\in_array($typeName, ['array', 'iterable'], true)) { |
||
| 551 | return $this->get($itemType); |
||
| 552 | } |
||
| 553 | |||
| 554 | if ('object' === $typeName || \is_subclass_of($itemType, $typeName)) { |
||
| 555 | return $this->get($itemType, true); |
||
| 556 | } |
||
| 557 | } catch (NotFoundServiceException $e) { |
||
| 558 | // Ignore this exception ... |
||
| 559 | } |
||
| 560 | } |
||
| 561 | |||
| 562 | if (isset($errorException)) { |
||
| 563 | throw $errorException; |
||
| 564 | } |
||
| 565 | } |
||
| 566 | |||
| 567 | return null; |
||
| 568 | } |
||
| 569 | |||
| 570 | /** |
||
| 571 | * Returns an associated type to the given parameter if available. |
||
| 572 | * |
||
| 573 | * @param \ReflectionParameter|\ReflectionFunctionAbstract $reflection |
||
| 574 | * |
||
| 575 | * @return array<int,string> |
||
| 576 | */ |
||
| 577 | private static function getTypes(\Reflector $reflection): array |
||
| 612 | } |
||
| 613 | |||
| 614 | /** |
||
| 615 | * Get the parameter's allowed null else error. |
||
| 616 | * |
||
| 617 | * @throws \ReflectionException|ContainerResolutionException |
||
| 618 | * |
||
| 619 | * @return null |
||
| 620 | */ |
||
| 621 | private static function getParameterDefaultValue(\ReflectionParameter $parameter, array $types) |
||
| 642 | } |
||
| 643 | } |
||
| 644 |