| Total Complexity | 194 |
| Total Lines | 713 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like AutowirePass 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 AutowirePass, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 36 | class AutowirePass extends AbstractRecursivePass |
||
| 37 | { |
||
| 38 | protected bool $skipScalars = true; |
||
| 39 | |||
| 40 | private array $types; |
||
| 41 | private array $ambiguousServiceTypes; |
||
| 42 | private array $autowiringAliases; |
||
| 43 | private ?string $lastFailure = null; |
||
| 44 | private ?string $decoratedClass = null; |
||
| 45 | private ?string $decoratedId = null; |
||
| 46 | private object $defaultArgument; |
||
| 47 | private ?\Closure $restorePreviousValue = null; |
||
| 48 | private ?self $typesClone = null; |
||
| 49 | |||
| 50 | public function __construct( |
||
| 51 | private bool $throwOnAutowiringException = true, |
||
| 52 | ) { |
||
| 53 | $this->defaultArgument = new class { |
||
| 54 | public $value; |
||
| 55 | public $names; |
||
| 56 | public $bag; |
||
| 57 | |||
| 58 | public function withValue(\ReflectionParameter $parameter): self |
||
| 59 | { |
||
| 60 | $clone = clone $this; |
||
| 61 | $clone->value = $this->bag->escapeValue($parameter->getDefaultValue()); |
||
| 62 | |||
| 63 | return $clone; |
||
| 64 | } |
||
| 65 | }; |
||
| 66 | } |
||
| 67 | |||
| 68 | public function process(ContainerBuilder $container): void |
||
| 69 | { |
||
| 70 | $this->defaultArgument->bag = $container->getParameterBag(); |
||
| 71 | |||
| 72 | try { |
||
| 73 | $this->typesClone = clone $this; |
||
| 74 | parent::process($container); |
||
| 75 | } finally { |
||
| 76 | $this->decoratedClass = null; |
||
| 77 | $this->decoratedId = null; |
||
| 78 | $this->defaultArgument->bag = null; |
||
| 79 | $this->defaultArgument->names = null; |
||
| 80 | $this->restorePreviousValue = null; |
||
| 81 | $this->typesClone = null; |
||
| 82 | } |
||
| 83 | } |
||
| 84 | |||
| 85 | protected function processValue(mixed $value, bool $isRoot = false): mixed |
||
| 86 | { |
||
| 87 | if ($value instanceof Autowire) { |
||
| 88 | return $this->processValue($this->container->getParameterBag()->resolveValue($value->value)); |
||
| 89 | } |
||
| 90 | |||
| 91 | if ($value instanceof AutowireDecorated) { |
||
| 92 | $definition = $this->container->getDefinition($this->currentId); |
||
| 93 | |||
| 94 | return new Reference($definition->innerServiceId ?? $this->currentId.'.inner', $definition->decorationOnInvalid ?? ContainerInterface::NULL_ON_INVALID_REFERENCE); |
||
| 95 | } |
||
| 96 | |||
| 97 | try { |
||
| 98 | return $this->doProcessValue($value, $isRoot); |
||
| 99 | } catch (AutowiringFailedException $e) { |
||
| 100 | if ($this->throwOnAutowiringException) { |
||
| 101 | throw $e; |
||
| 102 | } |
||
| 103 | |||
| 104 | $this->container->getDefinition($this->currentId)->addError($e->getMessageCallback() ?? $e->getMessage()); |
||
| 105 | |||
| 106 | return parent::processValue($value, $isRoot); |
||
| 107 | } |
||
| 108 | } |
||
| 109 | |||
| 110 | private function doProcessValue(mixed $value, bool $isRoot = false): mixed |
||
| 111 | { |
||
| 112 | if ($value instanceof TypedReference) { |
||
| 113 | foreach ($value->getAttributes() as $attribute) { |
||
| 114 | if ($attribute === $v = $this->processValue($attribute)) { |
||
| 115 | continue; |
||
| 116 | } |
||
| 117 | if (!$attribute instanceof Autowire || !$v instanceof Reference) { |
||
| 118 | return $v; |
||
| 119 | } |
||
| 120 | |||
| 121 | $invalidBehavior = ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE !== $v->getInvalidBehavior() ? $v->getInvalidBehavior() : $value->getInvalidBehavior(); |
||
| 122 | $value = $v instanceof TypedReference |
||
| 123 | ? new TypedReference($v, $v->getType(), $invalidBehavior, $v->getName() ?? $value->getName(), array_merge($v->getAttributes(), $value->getAttributes())) |
||
| 124 | : new TypedReference($v, $value->getType(), $invalidBehavior, $value->getName(), $value->getAttributes()); |
||
| 125 | break; |
||
| 126 | } |
||
| 127 | if ($ref = $this->getAutowiredReference($value, true)) { |
||
| 128 | return $ref; |
||
| 129 | } |
||
| 130 | if (ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) { |
||
| 131 | $message = $this->createTypeNotFoundMessageCallback($value, 'it'); |
||
| 132 | |||
| 133 | // since the error message varies by referenced id and $this->currentId, so should the id of the dummy errored definition |
||
| 134 | $this->container->register($id = \sprintf('.errored.%s.%s', $this->currentId, (string) $value), $value->getType()) |
||
| 135 | ->addError($message); |
||
| 136 | |||
| 137 | return new TypedReference($id, $value->getType(), $value->getInvalidBehavior(), $value->getName()); |
||
| 138 | } |
||
| 139 | } |
||
| 140 | $value = parent::processValue($value, $isRoot); |
||
| 141 | |||
| 142 | if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) { |
||
| 143 | return $value; |
||
| 144 | } |
||
| 145 | if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) { |
||
| 146 | $this->container->log($this, \sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass())); |
||
| 147 | |||
| 148 | return $value; |
||
| 149 | } |
||
| 150 | |||
| 151 | $methodCalls = $value->getMethodCalls(); |
||
| 152 | |||
| 153 | try { |
||
| 154 | $constructor = $this->getConstructor($value, false); |
||
| 155 | } catch (RuntimeException $e) { |
||
| 156 | throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e); |
||
| 157 | } |
||
| 158 | |||
| 159 | if ($constructor) { |
||
| 160 | array_unshift($methodCalls, [$constructor, $value->getArguments()]); |
||
| 161 | } |
||
| 162 | |||
| 163 | $checkAttributes = !$value->hasTag('container.ignore_attributes'); |
||
| 164 | $methodCalls = $this->autowireCalls($methodCalls, $reflectionClass, $isRoot, $checkAttributes); |
||
| 165 | |||
| 166 | if ($constructor) { |
||
| 167 | [, $arguments] = array_shift($methodCalls); |
||
| 168 | |||
| 169 | if ($arguments !== $value->getArguments()) { |
||
| 170 | $value->setArguments($arguments); |
||
| 171 | } |
||
| 172 | } |
||
| 173 | |||
| 174 | if ($methodCalls !== $value->getMethodCalls()) { |
||
| 175 | $value->setMethodCalls($methodCalls); |
||
| 176 | } |
||
| 177 | |||
| 178 | return $value; |
||
| 179 | } |
||
| 180 | |||
| 181 | private function autowireCalls(array $methodCalls, \ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array |
||
| 250 | } |
||
| 251 | |||
| 252 | /** |
||
| 253 | * Autowires the constructor or a method. |
||
| 254 | * |
||
| 255 | * @throws AutowiringFailedException |
||
| 256 | */ |
||
| 257 | private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments, bool $checkAttributes): array |
||
| 258 | { |
||
| 259 | $class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId; |
||
| 260 | $method = $reflectionMethod->name; |
||
| 261 | $parameters = $reflectionMethod->getParameters(); |
||
| 262 | if ($reflectionMethod->isVariadic()) { |
||
| 263 | array_pop($parameters); |
||
| 264 | } |
||
| 265 | $defaultArgument = clone $this->defaultArgument; |
||
| 266 | $defaultArgument->names = new \ArrayObject(); |
||
| 267 | |||
| 268 | foreach ($parameters as $index => $parameter) { |
||
| 269 | $defaultArgument->names[$index] = $parameter->name; |
||
| 270 | |||
| 271 | if (\array_key_exists($parameter->name, $arguments)) { |
||
| 272 | $arguments[$index] = $arguments[$parameter->name]; |
||
| 273 | unset($arguments[$parameter->name]); |
||
| 274 | } |
||
| 275 | if (\array_key_exists($index, $arguments) && '' !== $arguments[$index]) { |
||
| 276 | continue; |
||
| 277 | } |
||
| 278 | |||
| 279 | $type = ProxyHelper::exportType($parameter, true); |
||
| 280 | $target = null; |
||
| 281 | $name = Target::parseName($parameter, $target); |
||
| 282 | $target = $target ? [$target] : []; |
||
| 283 | $currentId = $this->currentId; |
||
| 284 | |||
| 285 | $getValue = function () use ($type, $parameter, $class, $method, $name, $target, $defaultArgument, $currentId) { |
||
| 286 | if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $name, $target), false)) { |
||
| 287 | $failureMessage = $this->createTypeNotFoundMessageCallback($ref, \sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $currentId ? $class.'::'.$method : $method)); |
||
| 288 | |||
| 289 | if ($parameter->isDefaultValueAvailable()) { |
||
| 290 | $value = $defaultArgument->withValue($parameter); |
||
| 291 | } elseif (!$parameter->allowsNull()) { |
||
| 292 | throw new AutowiringFailedException($currentId, $failureMessage); |
||
| 293 | } |
||
| 294 | } |
||
| 295 | |||
| 296 | return $value; |
||
| 297 | }; |
||
| 298 | |||
| 299 | if ($checkAttributes) { |
||
| 300 | $attributes = array_merge($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF), $parameter->getAttributes(Lazy::class, \ReflectionAttribute::IS_INSTANCEOF)); |
||
| 301 | |||
| 302 | if (1 < \count($attributes)) { |
||
| 303 | throw new AutowiringFailedException($this->currentId, 'Using both attributes #[Lazy] and #[Autowire] on an argument is not allowed; use the "lazy" parameter of #[Autowire] instead.'); |
||
| 304 | } |
||
| 305 | |||
| 306 | foreach ($attributes as $attribute) { |
||
| 307 | $attribute = $attribute->newInstance(); |
||
| 308 | $value = $attribute instanceof Autowire ? $attribute->value : null; |
||
| 309 | |||
| 310 | if (\is_string($value) && str_starts_with($value, '%env(') && str_ends_with($value, ')%')) { |
||
| 311 | if ($parameter->getType() instanceof \ReflectionNamedType && 'bool' === $parameter->getType()->getName() && !str_starts_with($value, '%env(bool:')) { |
||
| 312 | $attribute = new Autowire(substr_replace($value, 'bool:', 5, 0)); |
||
| 313 | } |
||
| 314 | if ($parameter->isDefaultValueAvailable() && $parameter->allowsNull() && null === $parameter->getDefaultValue() && !preg_match('/(^|:)default:/', $value)) { |
||
| 315 | $attribute = new Autowire(substr_replace($value, 'default::', 5, 0)); |
||
| 316 | } |
||
| 317 | } |
||
| 318 | |||
| 319 | $invalidBehavior = $parameter->allowsNull() ? ContainerInterface::NULL_ON_INVALID_REFERENCE : ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE; |
||
| 320 | |||
| 321 | try { |
||
| 322 | $value = $this->processValue(new TypedReference($type ?: '?', $type ?: 'mixed', $invalidBehavior, $name, [$attribute, ...$target])); |
||
| 323 | } catch (ParameterNotFoundException $e) { |
||
| 324 | if (!$parameter->isDefaultValueAvailable()) { |
||
| 325 | throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e); |
||
| 326 | } |
||
| 327 | $arguments[$index] = clone $defaultArgument; |
||
| 328 | $arguments[$index]->value = $parameter->getDefaultValue(); |
||
| 329 | |||
| 330 | continue 2; |
||
| 331 | } |
||
| 332 | |||
| 333 | if ($attribute instanceof AutowireInline) { |
||
| 334 | $value = $attribute->buildDefinition($value, $type, $parameter); |
||
| 335 | $value = $this->doProcessValue($value); |
||
| 336 | } elseif ($lazy = $attribute->lazy) { |
||
| 337 | $definition = (new Definition($type)) |
||
| 338 | ->setFactory('current') |
||
| 339 | ->setArguments([[$value ??= $getValue()]]) |
||
| 340 | ->setLazy(true); |
||
| 341 | |||
| 342 | if (!\is_array($lazy)) { |
||
| 343 | if (str_contains($type, '|')) { |
||
| 344 | throw new AutowiringFailedException($this->currentId, \sprintf('Cannot use #[Autowire] with option "lazy: true" on union types for service "%s"; set the option to the interface(s) that should be proxied instead.', $this->currentId)); |
||
| 345 | } |
||
| 346 | $lazy = str_contains($type, '&') ? explode('&', $type) : []; |
||
| 347 | } |
||
| 348 | |||
| 349 | if ($lazy) { |
||
| 350 | if (!class_exists($type) && !interface_exists($type, false)) { |
||
| 351 | $definition->setClass('object'); |
||
| 352 | } |
||
| 353 | foreach ($lazy as $v) { |
||
| 354 | $definition->addTag('proxy', ['interface' => $v]); |
||
| 355 | } |
||
| 356 | } |
||
| 357 | |||
| 358 | if ($definition->getClass() !== (string) $value || $definition->getTag('proxy')) { |
||
| 359 | $value .= '.'.$this->container->hash([$definition->getClass(), $definition->getTag('proxy')]); |
||
| 360 | } |
||
| 361 | $this->container->setDefinition($value = '.lazy.'.$value, $definition); |
||
| 362 | $value = new Reference($value); |
||
| 363 | } |
||
| 364 | $arguments[$index] = $value; |
||
| 365 | |||
| 366 | continue 2; |
||
| 367 | } |
||
| 368 | |||
| 369 | foreach ($parameter->getAttributes(AutowireDecorated::class) as $attribute) { |
||
| 370 | $arguments[$index] = $this->processValue($attribute->newInstance()); |
||
| 371 | |||
| 372 | continue 2; |
||
| 373 | } |
||
| 374 | } |
||
| 375 | |||
| 376 | if (!$type) { |
||
| 377 | if (isset($arguments[$index])) { |
||
| 378 | continue; |
||
| 379 | } |
||
| 380 | |||
| 381 | // no default value? Then fail |
||
| 382 | if (!$parameter->isDefaultValueAvailable()) { |
||
| 383 | // For core classes, isDefaultValueAvailable() can |
||
| 384 | // be false when isOptional() returns true. If the |
||
| 385 | // argument *is* optional, allow it to be missing |
||
| 386 | if ($parameter->isOptional()) { |
||
| 387 | --$index; |
||
| 388 | break; |
||
| 389 | } |
||
| 390 | $type = ProxyHelper::exportType($parameter); |
||
| 391 | $type = $type ? \sprintf('is type-hinted "%s"', preg_replace('/(^|[(|&])\\\\|^\?\\\\?/', '\1', $type)) : 'has no type-hint'; |
||
| 392 | |||
| 393 | throw new AutowiringFailedException($this->currentId, \sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" %s, you should configure its value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method, $type)); |
||
| 394 | } |
||
| 395 | |||
| 396 | // specifically pass the default value |
||
| 397 | $arguments[$index] = $defaultArgument->withValue($parameter); |
||
| 398 | |||
| 399 | continue; |
||
| 400 | } |
||
| 401 | |||
| 402 | if ($this->decoratedClass && is_a($this->decoratedClass, $type, true)) { |
||
| 403 | if ($this->restorePreviousValue) { |
||
| 404 | // The inner service is injected only if there is only 1 argument matching the type of the decorated class |
||
| 405 | // across all arguments of all autowired methods. |
||
| 406 | // If a second matching argument is found, the default behavior is restored. |
||
| 407 | ($this->restorePreviousValue)(); |
||
| 408 | $this->decoratedClass = $this->restorePreviousValue = null; // Prevent further checks |
||
| 409 | } else { |
||
| 410 | $arguments[$index] = new TypedReference($this->decoratedId, $this->decoratedClass); |
||
| 411 | $argumentAtIndex = &$arguments[$index]; |
||
| 412 | $this->restorePreviousValue = static function () use (&$argumentAtIndex, $getValue) { |
||
| 413 | $argumentAtIndex = $getValue(); |
||
| 414 | }; |
||
| 415 | |||
| 416 | continue; |
||
| 417 | } |
||
| 418 | } |
||
| 419 | |||
| 420 | $arguments[$index] = $getValue(); |
||
| 421 | } |
||
| 422 | |||
| 423 | if ($parameters && !isset($arguments[++$index])) { |
||
| 424 | while (0 <= --$index) { |
||
| 425 | if (!$arguments[$index] instanceof $defaultArgument) { |
||
| 426 | break; |
||
| 427 | } |
||
| 428 | unset($arguments[$index]); |
||
| 429 | } |
||
| 430 | } |
||
| 431 | |||
| 432 | // it's possible index 1 was set, then index 0, then 2, etc |
||
| 433 | // make sure that we re-order so they're injected as expected |
||
| 434 | ksort($arguments, \SORT_NATURAL); |
||
| 435 | |||
| 436 | return $arguments; |
||
| 437 | } |
||
| 438 | |||
| 439 | /** |
||
| 440 | * Returns a reference to the service matching the given type, if any. |
||
| 441 | */ |
||
| 442 | private function getAutowiredReference(TypedReference $reference, bool $filterType): ?TypedReference |
||
| 443 | { |
||
| 444 | $this->lastFailure = null; |
||
| 445 | $type = $reference->getType(); |
||
| 446 | |||
| 447 | if ($type !== (string) $reference) { |
||
| 448 | return $reference; |
||
| 449 | } |
||
| 450 | |||
| 451 | if ($filterType && false !== $m = strpbrk($type, '&|')) { |
||
| 452 | $types = array_diff(explode($m[0], $type), ['int', 'string', 'array', 'bool', 'float', 'iterable', 'object', 'callable', 'null']); |
||
| 453 | |||
| 454 | sort($types); |
||
| 455 | |||
| 456 | $type = implode($m[0], $types); |
||
| 457 | } |
||
| 458 | |||
| 459 | $name = $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)?->name; |
||
| 460 | |||
| 461 | if (null !== $name ??= $reference->getName()) { |
||
| 462 | if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) { |
||
| 463 | return new TypedReference($alias, $type, $reference->getInvalidBehavior()); |
||
| 464 | } |
||
| 465 | |||
| 466 | if (null !== ($alias = $this->getCombinedAlias($type, $name)) && !$this->container->findDefinition($alias)->isAbstract()) { |
||
| 467 | return new TypedReference($alias, $type, $reference->getInvalidBehavior()); |
||
| 468 | } |
||
| 469 | |||
| 470 | $parsedName = (new Target($name))->getParsedName(); |
||
| 471 | |||
| 472 | if ($this->container->has($alias = $type.' $'.$parsedName) && !$this->container->findDefinition($alias)->isAbstract()) { |
||
| 473 | return new TypedReference($alias, $type, $reference->getInvalidBehavior()); |
||
| 474 | } |
||
| 475 | |||
| 476 | if (null !== ($alias = $this->getCombinedAlias($type, $parsedName)) && !$this->container->findDefinition($alias)->isAbstract()) { |
||
| 477 | return new TypedReference($alias, $type, $reference->getInvalidBehavior()); |
||
| 478 | } |
||
| 479 | |||
| 480 | if (($this->container->has($n = $name) && !$this->container->findDefinition($n)->isAbstract()) |
||
| 481 | || ($this->container->has($n = $parsedName) && !$this->container->findDefinition($n)->isAbstract()) |
||
| 482 | ) { |
||
| 483 | foreach ($this->container->getAliases() as $id => $alias) { |
||
| 484 | if ($n === (string) $alias && str_starts_with($id, $type.' $')) { |
||
| 485 | return new TypedReference($n, $type, $reference->getInvalidBehavior()); |
||
| 486 | } |
||
| 487 | } |
||
| 488 | } |
||
| 489 | |||
| 490 | if (null !== $target) { |
||
| 491 | return null; |
||
| 492 | } |
||
| 493 | } |
||
| 494 | |||
| 495 | if ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract()) { |
||
| 496 | return new TypedReference($type, $type, $reference->getInvalidBehavior()); |
||
| 497 | } |
||
| 498 | |||
| 499 | if (null !== ($alias = $this->getCombinedAlias($type)) && !$this->container->findDefinition($alias)->isAbstract()) { |
||
| 500 | return new TypedReference($alias, $type, $reference->getInvalidBehavior()); |
||
| 501 | } |
||
| 502 | |||
| 503 | return null; |
||
| 504 | } |
||
| 505 | |||
| 506 | /** |
||
| 507 | * Populates the list of available types. |
||
| 508 | */ |
||
| 509 | private function populateAvailableTypes(ContainerBuilder $container): void |
||
| 510 | { |
||
| 511 | $this->types = []; |
||
| 512 | $this->ambiguousServiceTypes = []; |
||
| 513 | $this->autowiringAliases = []; |
||
| 514 | |||
| 515 | foreach ($container->getDefinitions() as $id => $definition) { |
||
| 516 | $this->populateAvailableType($container, $id, $definition); |
||
| 517 | } |
||
| 518 | |||
| 519 | $prev = null; |
||
| 520 | foreach ($container->getAliases() as $id => $alias) { |
||
| 521 | $this->populateAutowiringAlias($id, $prev); |
||
| 522 | $prev = $id; |
||
| 523 | } |
||
| 524 | } |
||
| 525 | |||
| 526 | /** |
||
| 527 | * Populates the list of available types for a given definition. |
||
| 528 | */ |
||
| 529 | private function populateAvailableType(ContainerBuilder $container, string $id, Definition $definition): void |
||
| 530 | { |
||
| 531 | // Never use abstract services |
||
| 532 | if ($definition->isAbstract()) { |
||
| 533 | return; |
||
| 534 | } |
||
| 535 | |||
| 536 | if ('' === $id || '.' === $id[0] || $definition->isDeprecated() || !$reflectionClass = $container->getReflectionClass($definition->getClass(), false)) { |
||
| 537 | return; |
||
| 538 | } |
||
| 539 | |||
| 540 | foreach ($reflectionClass->getInterfaces() as $reflectionInterface) { |
||
| 541 | $this->set($reflectionInterface->name, $id); |
||
| 542 | } |
||
| 543 | |||
| 544 | do { |
||
| 545 | $this->set($reflectionClass->name, $id); |
||
| 546 | } while ($reflectionClass = $reflectionClass->getParentClass()); |
||
| 547 | |||
| 548 | $this->populateAutowiringAlias($id); |
||
| 549 | } |
||
| 550 | |||
| 551 | /** |
||
| 552 | * Associates a type and a service id if applicable. |
||
| 553 | */ |
||
| 554 | private function set(string $type, string $id): void |
||
| 555 | { |
||
| 556 | // is this already a type/class that is known to match multiple services? |
||
| 557 | if (isset($this->ambiguousServiceTypes[$type])) { |
||
| 558 | $this->ambiguousServiceTypes[$type][] = $id; |
||
| 559 | |||
| 560 | return; |
||
| 561 | } |
||
| 562 | |||
| 563 | // check to make sure the type doesn't match multiple services |
||
| 564 | if (!isset($this->types[$type]) || $this->types[$type] === $id) { |
||
| 565 | $this->types[$type] = $id; |
||
| 566 | |||
| 567 | return; |
||
| 568 | } |
||
| 569 | |||
| 570 | // keep an array of all services matching this type |
||
| 571 | if (!isset($this->ambiguousServiceTypes[$type])) { |
||
| 572 | $this->ambiguousServiceTypes[$type] = [$this->types[$type]]; |
||
| 573 | unset($this->types[$type]); |
||
| 574 | } |
||
| 575 | $this->ambiguousServiceTypes[$type][] = $id; |
||
| 576 | } |
||
| 577 | |||
| 578 | private function createTypeNotFoundMessageCallback(TypedReference $reference, string $label): \Closure |
||
| 579 | { |
||
| 580 | if (!isset($this->typesClone->container)) { |
||
| 581 | $this->typesClone->container = new ContainerBuilder($this->container->getParameterBag()); |
||
| 582 | $this->typesClone->container->setAliases($this->container->getAliases()); |
||
| 583 | $this->typesClone->container->setDefinitions($this->container->getDefinitions()); |
||
| 584 | $this->typesClone->container->setResourceTracking(false); |
||
| 585 | } |
||
| 586 | $currentId = $this->currentId; |
||
| 587 | |||
| 588 | return (fn () => $this->createTypeNotFoundMessage($reference, $label, $currentId))->bindTo($this->typesClone); |
||
| 589 | } |
||
| 590 | |||
| 591 | private function createTypeNotFoundMessage(TypedReference $reference, string $label, string $currentId): string |
||
| 592 | { |
||
| 593 | $type = $reference->getType(); |
||
| 594 | |||
| 595 | $i = null; |
||
| 596 | $namespace = $type; |
||
| 597 | do { |
||
| 598 | $namespace = substr($namespace, 0, $i); |
||
| 599 | |||
| 600 | if ($this->container->hasDefinition($namespace) && $tag = $this->container->getDefinition($namespace)->getTag('container.excluded')) { |
||
| 601 | return \sprintf('Cannot autowire service "%s": %s needs an instance of "%s" but this type has been excluded %s.', $currentId, $label, $type, $tag[0]['source'] ?? 'from autowiring'); |
||
| 602 | } |
||
| 603 | } while (false !== $i = strrpos($namespace, '\\')); |
||
| 604 | |||
| 605 | if (!$r = $this->container->getReflectionClass($type, false)) { |
||
| 606 | // either $type does not exist or a parent class does not exist |
||
| 607 | try { |
||
| 608 | if (class_exists(ClassExistenceResource::class)) { |
||
| 609 | $resource = new ClassExistenceResource($type, false); |
||
| 610 | // isFresh() will explode ONLY if a parent class/trait does not exist |
||
| 611 | $resource->isFresh(0); |
||
| 612 | $parentMsg = false; |
||
| 613 | } else { |
||
| 614 | $parentMsg = "couldn't be loaded. Either it was not found or it is missing a parent class or a trait"; |
||
| 615 | } |
||
| 616 | } catch (\ReflectionException $e) { |
||
| 617 | $parentMsg = \sprintf('is missing a parent class (%s)', $e->getMessage()); |
||
| 618 | } |
||
| 619 | |||
| 620 | $message = \sprintf('has type "%s" but this class %s.', $type, $parentMsg ?: 'was not found'); |
||
| 621 | } else { |
||
| 622 | $alternatives = $this->createTypeAlternatives($this->container, $reference); |
||
| 623 | |||
| 624 | if (null !== $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)) { |
||
| 625 | $target = null !== $target->name ? "('{$target->name}')" : ''; |
||
| 626 | $message = \sprintf('has "#[Target%s]" but no such target exists.%s', $target, $alternatives); |
||
| 627 | } else { |
||
| 628 | $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists'; |
||
| 629 | $message = \sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $alternatives); |
||
| 630 | } |
||
| 631 | |||
| 632 | if ($r->isInterface() && !$alternatives) { |
||
| 633 | $message .= ' Did you create an instantiable class that implements this interface?'; |
||
| 634 | } |
||
| 635 | } |
||
| 636 | |||
| 637 | $message = \sprintf('Cannot autowire service "%s": %s %s', $currentId, $label, $message); |
||
| 638 | |||
| 639 | if (null !== $this->lastFailure) { |
||
| 640 | $message = $this->lastFailure."\n".$message; |
||
| 641 | $this->lastFailure = null; |
||
| 642 | } |
||
| 643 | |||
| 644 | return $message; |
||
| 645 | } |
||
| 646 | |||
| 647 | private function createTypeAlternatives(ContainerBuilder $container, TypedReference $reference): string |
||
| 648 | { |
||
| 649 | // try suggesting available aliases first |
||
| 650 | if ($message = $this->getAliasesSuggestionForType($container, $type = $reference->getType())) { |
||
| 651 | return ' '.$message; |
||
| 652 | } |
||
| 653 | if (!isset($this->ambiguousServiceTypes)) { |
||
| 654 | $this->populateAvailableTypes($container); |
||
| 655 | } |
||
| 656 | |||
| 657 | $servicesAndAliases = $container->getServiceIds(); |
||
| 658 | $autowiringAliases = $this->autowiringAliases[$type] ?? []; |
||
| 659 | unset($autowiringAliases['']); |
||
| 660 | |||
| 661 | if ($autowiringAliases) { |
||
| 662 | return \sprintf(' Did you mean to target%s "%s" instead?', 1 < \count($autowiringAliases) ? ' one of' : '', implode('", "', $autowiringAliases)); |
||
| 663 | } |
||
| 664 | |||
| 665 | if (!$container->has($type) && false !== $key = array_search(strtolower($type), array_map('strtolower', $servicesAndAliases))) { |
||
| 666 | return \sprintf(' Did you mean "%s"?', $servicesAndAliases[$key]); |
||
| 667 | } elseif (isset($this->ambiguousServiceTypes[$type])) { |
||
| 668 | $message = \sprintf('one of these existing services: "%s"', implode('", "', $this->ambiguousServiceTypes[$type])); |
||
| 669 | } elseif (isset($this->types[$type])) { |
||
| 670 | $message = \sprintf('the existing "%s" service', $this->types[$type]); |
||
| 671 | } else { |
||
| 672 | return ''; |
||
| 673 | } |
||
| 674 | |||
| 675 | return \sprintf(' You should maybe alias this %s to %s.', class_exists($type, false) ? 'class' : 'interface', $message); |
||
| 676 | } |
||
| 677 | |||
| 678 | private function getAliasesSuggestionForType(ContainerBuilder $container, string $type): ?string |
||
| 679 | { |
||
| 680 | $aliases = []; |
||
| 681 | foreach (class_parents($type) + class_implements($type) as $parent) { |
||
| 682 | if ($container->has($parent) && !$container->findDefinition($parent)->isAbstract()) { |
||
| 683 | $aliases[] = $parent; |
||
| 684 | } |
||
| 685 | } |
||
| 686 | |||
| 687 | if (1 < $len = \count($aliases)) { |
||
| 688 | $message = 'Try changing the type-hint to one of its parents: '; |
||
| 689 | for ($i = 0, --$len; $i < $len; ++$i) { |
||
| 690 | $message .= \sprintf('%s "%s", ', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]); |
||
| 691 | } |
||
| 692 | |||
| 693 | return $message.\sprintf('or %s "%s".', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]); |
||
| 694 | } |
||
| 695 | |||
| 696 | if ($aliases) { |
||
| 697 | return \sprintf('Try changing the type-hint to "%s" instead.', $aliases[0]); |
||
| 698 | } |
||
| 699 | |||
| 700 | return null; |
||
| 701 | } |
||
| 702 | |||
| 703 | private function populateAutowiringAlias(string $id, ?string $target = null): void |
||
| 720 | } |
||
| 721 | } |
||
| 722 | |||
| 723 | private function getCombinedAlias(string $type, ?string $name = null): ?string |
||
| 724 | { |
||
| 725 | if (str_contains($type, '&')) { |
||
| 726 | $types = explode('&', $type); |
||
| 727 | } elseif (str_contains($type, '|')) { |
||
| 728 | $types = explode('|', $type); |
||
| 729 | } else { |
||
| 749 | } |
||
| 750 | } |
||
| 751 |
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