vendor/symfony/dependency-injection/Compiler/AutowirePass.php line 426

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\DependencyInjection\Compiler;
  11. use Symfony\Component\Config\Resource\ClassExistenceResource;
  12. use Symfony\Component\DependencyInjection\ContainerBuilder;
  13. use Symfony\Component\DependencyInjection\Definition;
  14. use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
  15. use Symfony\Component\DependencyInjection\Exception\RuntimeException;
  16. use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
  17. use Symfony\Component\DependencyInjection\TypedReference;
  18. /**
  19.  * Inspects existing service definitions and wires the autowired ones using the type hints of their classes.
  20.  *
  21.  * @author Kévin Dunglas <dunglas@gmail.com>
  22.  * @author Nicolas Grekas <p@tchwork.com>
  23.  */
  24. class AutowirePass extends AbstractRecursivePass
  25. {
  26.     private $types;
  27.     private $ambiguousServiceTypes;
  28.     private $lastFailure;
  29.     private $throwOnAutowiringException;
  30.     private $decoratedClass;
  31.     private $decoratedId;
  32.     private $methodCalls;
  33.     private $getPreviousValue;
  34.     private $decoratedMethodIndex;
  35.     private $decoratedMethodArgumentIndex;
  36.     private $typesClone;
  37.     public function __construct(bool $throwOnAutowireException true)
  38.     {
  39.         $this->throwOnAutowiringException $throwOnAutowireException;
  40.     }
  41.     /**
  42.      * {@inheritdoc}
  43.      */
  44.     public function process(ContainerBuilder $container)
  45.     {
  46.         try {
  47.             $this->typesClone = clone $this;
  48.             parent::process($container);
  49.         } finally {
  50.             $this->decoratedClass null;
  51.             $this->decoratedId null;
  52.             $this->methodCalls null;
  53.             $this->getPreviousValue null;
  54.             $this->decoratedMethodIndex null;
  55.             $this->decoratedMethodArgumentIndex null;
  56.             $this->typesClone null;
  57.         }
  58.     }
  59.     /**
  60.      * {@inheritdoc}
  61.      */
  62.     protected function processValue($valuebool $isRoot false)
  63.     {
  64.         try {
  65.             return $this->doProcessValue($value$isRoot);
  66.         } catch (AutowiringFailedException $e) {
  67.             if ($this->throwOnAutowiringException) {
  68.                 throw $e;
  69.             }
  70.             $this->container->getDefinition($this->currentId)->addError($e->getMessageCallback() ?? $e->getMessage());
  71.             return parent::processValue($value$isRoot);
  72.         }
  73.     }
  74.     /**
  75.      * @return mixed
  76.      */
  77.     private function doProcessValue($valuebool $isRoot false)
  78.     {
  79.         if ($value instanceof TypedReference) {
  80.             if ($ref $this->getAutowiredReference($value)) {
  81.                 return $ref;
  82.             }
  83.             if (ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) {
  84.                 $message $this->createTypeNotFoundMessageCallback($value'it');
  85.                 // since the error message varies by referenced id and $this->currentId, so should the id of the dummy errored definition
  86.                 $this->container->register($id sprintf('.errored.%s.%s'$this->currentId, (string) $value), $value->getType())
  87.                     ->addError($message);
  88.                 return new TypedReference($id$value->getType(), $value->getInvalidBehavior(), $value->getName());
  89.             }
  90.         }
  91.         $value parent::processValue($value$isRoot);
  92.         if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
  93.             return $value;
  94.         }
  95.         if (!$reflectionClass $this->container->getReflectionClass($value->getClass(), false)) {
  96.             $this->container->log($thissprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.'$this->currentId$value->getClass()));
  97.             return $value;
  98.         }
  99.         $this->methodCalls $value->getMethodCalls();
  100.         try {
  101.             $constructor $this->getConstructor($valuefalse);
  102.         } catch (RuntimeException $e) {
  103.             throw new AutowiringFailedException($this->currentId$e->getMessage(), 0$e);
  104.         }
  105.         if ($constructor) {
  106.             array_unshift($this->methodCalls, [$constructor$value->getArguments()]);
  107.         }
  108.         $this->methodCalls $this->autowireCalls($reflectionClass$isRoot);
  109.         if ($constructor) {
  110.             [, $arguments] = array_shift($this->methodCalls);
  111.             if ($arguments !== $value->getArguments()) {
  112.                 $value->setArguments($arguments);
  113.             }
  114.         }
  115.         if ($this->methodCalls !== $value->getMethodCalls()) {
  116.             $value->setMethodCalls($this->methodCalls);
  117.         }
  118.         return $value;
  119.     }
  120.     private function autowireCalls(\ReflectionClass $reflectionClassbool $isRoot): array
  121.     {
  122.         $this->decoratedId null;
  123.         $this->decoratedClass null;
  124.         $this->getPreviousValue null;
  125.         if ($isRoot && ($definition $this->container->getDefinition($this->currentId)) && null !== ($this->decoratedId $definition->innerServiceId) && $this->container->has($this->decoratedId)) {
  126.             $this->decoratedClass $this->container->findDefinition($this->decoratedId)->getClass();
  127.         }
  128.         foreach ($this->methodCalls as $i => $call) {
  129.             $this->decoratedMethodIndex $i;
  130.             [$method$arguments] = $call;
  131.             if ($method instanceof \ReflectionFunctionAbstract) {
  132.                 $reflectionMethod $method;
  133.             } else {
  134.                 $definition = new Definition($reflectionClass->name);
  135.                 try {
  136.                     $reflectionMethod $this->getReflectionMethod($definition$method);
  137.                 } catch (RuntimeException $e) {
  138.                     if ($definition->getFactory()) {
  139.                         continue;
  140.                     }
  141.                     throw $e;
  142.                 }
  143.             }
  144.             $arguments $this->autowireMethod($reflectionMethod$arguments);
  145.             if ($arguments !== $call[1]) {
  146.                 $this->methodCalls[$i][1] = $arguments;
  147.             }
  148.         }
  149.         return $this->methodCalls;
  150.     }
  151.     /**
  152.      * Autowires the constructor or a method.
  153.      *
  154.      * @return array The autowired arguments
  155.      *
  156.      * @throws AutowiringFailedException
  157.      */
  158.     private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments): array
  159.     {
  160.         $class $reflectionMethod instanceof \ReflectionMethod $reflectionMethod->class $this->currentId;
  161.         $method $reflectionMethod->name;
  162.         $parameters $reflectionMethod->getParameters();
  163.         if ($reflectionMethod->isVariadic()) {
  164.             array_pop($parameters);
  165.         }
  166.         foreach ($parameters as $index => $parameter) {
  167.             if (\array_key_exists($index$arguments) && '' !== $arguments[$index]) {
  168.                 continue;
  169.             }
  170.             $type ProxyHelper::getTypeHint($reflectionMethod$parametertrue);
  171.             if (!$type) {
  172.                 if (isset($arguments[$index])) {
  173.                     continue;
  174.                 }
  175.                 // no default value? Then fail
  176.                 if (!$parameter->isDefaultValueAvailable()) {
  177.                     // For core classes, isDefaultValueAvailable() can
  178.                     // be false when isOptional() returns true. If the
  179.                     // argument *is* optional, allow it to be missing
  180.                     if ($parameter->isOptional()) {
  181.                         continue;
  182.                     }
  183.                     $type ProxyHelper::getTypeHint($reflectionMethod$parameterfalse);
  184.                     $type $type sprintf('is type-hinted "%s"'ltrim($type'\\')) : 'has no type-hint';
  185.                     throw new AutowiringFailedException($this->currentIdsprintf('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));
  186.                 }
  187.                 // specifically pass the default value
  188.                 $arguments[$index] = $parameter->getDefaultValue();
  189.                 continue;
  190.             }
  191.             $getValue = function () use ($type$parameter$class$method) {
  192.                 if (!$value $this->getAutowiredReference($ref = new TypedReference($type$typeContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE$parameter->name))) {
  193.                     $failureMessage $this->createTypeNotFoundMessageCallback($refsprintf('argument "$%s" of method "%s()"'$parameter->name$class !== $this->currentId $class.'::'.$method $method));
  194.                     if ($parameter->isDefaultValueAvailable()) {
  195.                         $value $parameter->getDefaultValue();
  196.                     } elseif (!$parameter->allowsNull()) {
  197.                         throw new AutowiringFailedException($this->currentId$failureMessage);
  198.                     }
  199.                 }
  200.                 return $value;
  201.             };
  202.             if ($this->decoratedClass && $isDecorated is_a($this->decoratedClass$typetrue)) {
  203.                 if ($this->getPreviousValue) {
  204.                     // The inner service is injected only if there is only 1 argument matching the type of the decorated class
  205.                     // across all arguments of all autowired methods.
  206.                     // If a second matching argument is found, the default behavior is restored.
  207.                     $getPreviousValue $this->getPreviousValue;
  208.                     $this->methodCalls[$this->decoratedMethodIndex][1][$this->decoratedMethodArgumentIndex] = $getPreviousValue();
  209.                     $this->decoratedClass null// Prevent further checks
  210.                 } else {
  211.                     $arguments[$index] = new TypedReference($this->decoratedId$this->decoratedClass);
  212.                     $this->getPreviousValue $getValue;
  213.                     $this->decoratedMethodArgumentIndex $index;
  214.                     continue;
  215.                 }
  216.             }
  217.             $arguments[$index] = $getValue();
  218.         }
  219.         if ($parameters && !isset($arguments[++$index])) {
  220.             while (<= --$index) {
  221.                 $parameter $parameters[$index];
  222.                 if (!$parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== $arguments[$index]) {
  223.                     break;
  224.                 }
  225.                 unset($arguments[$index]);
  226.             }
  227.         }
  228.         // it's possible index 1 was set, then index 0, then 2, etc
  229.         // make sure that we re-order so they're injected as expected
  230.         ksort($arguments);
  231.         return $arguments;
  232.     }
  233.     /**
  234.      * Returns a reference to the service matching the given type, if any.
  235.      */
  236.     private function getAutowiredReference(TypedReference $reference): ?TypedReference
  237.     {
  238.         $this->lastFailure null;
  239.         $type $reference->getType();
  240.         if ($type !== (string) $reference) {
  241.             return $reference;
  242.         }
  243.         if (null !== $name $reference->getName()) {
  244.             if ($this->container->has($alias $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) {
  245.                 return new TypedReference($alias$type$reference->getInvalidBehavior());
  246.             }
  247.             if ($this->container->has($name) && !$this->container->findDefinition($name)->isAbstract()) {
  248.                 foreach ($this->container->getAliases() as $id => $alias) {
  249.                     if ($name === (string) $alias && str_starts_with($id$type.' $')) {
  250.                         return new TypedReference($name$type$reference->getInvalidBehavior());
  251.                     }
  252.                 }
  253.             }
  254.         }
  255.         if ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract()) {
  256.             return new TypedReference($type$type$reference->getInvalidBehavior());
  257.         }
  258.         return null;
  259.     }
  260.     /**
  261.      * Populates the list of available types.
  262.      */
  263.     private function populateAvailableTypes(ContainerBuilder $container)
  264.     {
  265.         $this->types = [];
  266.         $this->ambiguousServiceTypes = [];
  267.         foreach ($container->getDefinitions() as $id => $definition) {
  268.             $this->populateAvailableType($container$id$definition);
  269.         }
  270.     }
  271.     /**
  272.      * Populates the list of available types for a given definition.
  273.      */
  274.     private function populateAvailableType(ContainerBuilder $containerstring $idDefinition $definition)
  275.     {
  276.         // Never use abstract services
  277.         if ($definition->isAbstract()) {
  278.             return;
  279.         }
  280.         if ('' === $id || '.' === $id[0] || $definition->isDeprecated() || !$reflectionClass $container->getReflectionClass($definition->getClass(), false)) {
  281.             return;
  282.         }
  283.         foreach ($reflectionClass->getInterfaces() as $reflectionInterface) {
  284.             $this->set($reflectionInterface->name$id);
  285.         }
  286.         do {
  287.             $this->set($reflectionClass->name$id);
  288.         } while ($reflectionClass $reflectionClass->getParentClass());
  289.     }
  290.     /**
  291.      * Associates a type and a service id if applicable.
  292.      */
  293.     private function set(string $typestring $id)
  294.     {
  295.         // is this already a type/class that is known to match multiple services?
  296.         if (isset($this->ambiguousServiceTypes[$type])) {
  297.             $this->ambiguousServiceTypes[$type][] = $id;
  298.             return;
  299.         }
  300.         // check to make sure the type doesn't match multiple services
  301.         if (!isset($this->types[$type]) || $this->types[$type] === $id) {
  302.             $this->types[$type] = $id;
  303.             return;
  304.         }
  305.         // keep an array of all services matching this type
  306.         if (!isset($this->ambiguousServiceTypes[$type])) {
  307.             $this->ambiguousServiceTypes[$type] = [$this->types[$type]];
  308.             unset($this->types[$type]);
  309.         }
  310.         $this->ambiguousServiceTypes[$type][] = $id;
  311.     }
  312.     private function createTypeNotFoundMessageCallback(TypedReference $referencestring $label): \Closure
  313.     {
  314.         if (null === $this->typesClone->container) {
  315.             $this->typesClone->container = new ContainerBuilder($this->container->getParameterBag());
  316.             $this->typesClone->container->setAliases($this->container->getAliases());
  317.             $this->typesClone->container->setDefinitions($this->container->getDefinitions());
  318.             $this->typesClone->container->setResourceTracking(false);
  319.         }
  320.         $currentId $this->currentId;
  321.         return (function () use ($reference$label$currentId) {
  322.             return $this->createTypeNotFoundMessage($reference$label$currentId);
  323.         })->bindTo($this->typesClone);
  324.     }
  325.     private function createTypeNotFoundMessage(TypedReference $referencestring $labelstring $currentId): string
  326.     {
  327.         if (!$r $this->container->getReflectionClass($type $reference->getType(), false)) {
  328.             // either $type does not exist or a parent class does not exist
  329.             try {
  330.                 $resource = new ClassExistenceResource($typefalse);
  331.                 // isFresh() will explode ONLY if a parent class/trait does not exist
  332.                 $resource->isFresh(0);
  333.                 $parentMsg false;
  334.             } catch (\ReflectionException $e) {
  335.                 $parentMsg $e->getMessage();
  336.             }
  337.             $message sprintf('has type "%s" but this class %s.'$type$parentMsg sprintf('is missing a parent class (%s)'$parentMsg) : 'was not found');
  338.         } else {
  339.             $alternatives $this->createTypeAlternatives($this->container$reference);
  340.             $message $this->container->has($type) ? 'this service is abstract' 'no such service exists';
  341.             $message sprintf('references %s "%s" but %s.%s'$r->isInterface() ? 'interface' 'class'$type$message$alternatives);
  342.             if ($r->isInterface() && !$alternatives) {
  343.                 $message .= ' Did you create a class that implements this interface?';
  344.             }
  345.         }
  346.         $message sprintf('Cannot autowire service "%s": %s %s'$currentId$label$message);
  347.         if (null !== $this->lastFailure) {
  348.             $message $this->lastFailure."\n".$message;
  349.             $this->lastFailure null;
  350.         }
  351.         return $message;
  352.     }
  353.     private function createTypeAlternatives(ContainerBuilder $containerTypedReference $reference): string
  354.     {
  355.         // try suggesting available aliases first
  356.         if ($message $this->getAliasesSuggestionForType($container$type $reference->getType())) {
  357.             return ' '.$message;
  358.         }
  359.         if (null === $this->ambiguousServiceTypes) {
  360.             $this->populateAvailableTypes($container);
  361.         }
  362.         $servicesAndAliases $container->getServiceIds();
  363.         if (!$container->has($type) && false !== $key array_search(strtolower($type), array_map('strtolower'$servicesAndAliases))) {
  364.             return sprintf(' Did you mean "%s"?'$servicesAndAliases[$key]);
  365.         } elseif (isset($this->ambiguousServiceTypes[$type])) {
  366.             $message sprintf('one of these existing services: "%s"'implode('", "'$this->ambiguousServiceTypes[$type]));
  367.         } elseif (isset($this->types[$type])) {
  368.             $message sprintf('the existing "%s" service'$this->types[$type]);
  369.         } else {
  370.             return '';
  371.         }
  372.         return sprintf(' You should maybe alias this %s to %s.'class_exists($typefalse) ? 'class' 'interface'$message);
  373.     }
  374.     private function getAliasesSuggestionForType(ContainerBuilder $containerstring $type): ?string
  375.     {
  376.         $aliases = [];
  377.         foreach (class_parents($type) + class_implements($type) as $parent) {
  378.             if ($container->has($parent) && !$container->findDefinition($parent)->isAbstract()) {
  379.                 $aliases[] = $parent;
  380.             }
  381.         }
  382.         if ($len \count($aliases)) {
  383.             $message 'Try changing the type-hint to one of its parents: ';
  384.             for ($i 0, --$len$i $len; ++$i) {
  385.                 $message .= sprintf('%s "%s", 'class_exists($aliases[$i], false) ? 'class' 'interface'$aliases[$i]);
  386.             }
  387.             $message .= sprintf('or %s "%s".'class_exists($aliases[$i], false) ? 'class' 'interface'$aliases[$i]);
  388.             return $message;
  389.         }
  390.         if ($aliases) {
  391.             return sprintf('Try changing the type-hint to "%s" instead.'$aliases[0]);
  392.         }
  393.         return null;
  394.     }
  395. }