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

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\Argument\ServiceLocatorArgument;
  13. use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
  14. use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
  15. use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
  16. use Symfony\Component\DependencyInjection\Attribute\Target;
  17. use Symfony\Component\DependencyInjection\ContainerBuilder;
  18. use Symfony\Component\DependencyInjection\Definition;
  19. use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
  20. use Symfony\Component\DependencyInjection\Exception\RuntimeException;
  21. use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
  22. use Symfony\Component\DependencyInjection\TypedReference;
  23. /**
  24. * Inspects existing service definitions and wires the autowired ones using the type hints of their classes.
  25. *
  26. * @author Kévin Dunglas <dunglas@gmail.com>
  27. * @author Nicolas Grekas <p@tchwork.com>
  28. */
  29. class AutowirePass extends AbstractRecursivePass
  30. {
  31. private $types;
  32. private $ambiguousServiceTypes;
  33. private $autowiringAliases;
  34. private $lastFailure;
  35. private $throwOnAutowiringException;
  36. private $decoratedClass;
  37. private $decoratedId;
  38. private $methodCalls;
  39. private $defaultArgument;
  40. private $getPreviousValue;
  41. private $decoratedMethodIndex;
  42. private $decoratedMethodArgumentIndex;
  43. private $typesClone;
  44. public function __construct(bool $throwOnAutowireException = true)
  45. {
  46. $this->throwOnAutowiringException = $throwOnAutowireException;
  47. $this->defaultArgument = new class() {
  48. public $value;
  49. public $names;
  50. public $bag;
  51. public function withValue(\ReflectionParameter $parameter): self
  52. {
  53. $clone = clone $this;
  54. $clone->value = $this->bag->escapeValue($parameter->getDefaultValue());
  55. return $clone;
  56. }
  57. };
  58. }
  59. /**
  60. * {@inheritdoc}
  61. */
  62. public function process(ContainerBuilder $container)
  63. {
  64. $this->defaultArgument->bag = $container->getParameterBag();
  65. try {
  66. $this->typesClone = clone $this;
  67. parent::process($container);
  68. } finally {
  69. $this->decoratedClass = null;
  70. $this->decoratedId = null;
  71. $this->methodCalls = null;
  72. $this->defaultArgument->bag = null;
  73. $this->defaultArgument->names = null;
  74. $this->getPreviousValue = null;
  75. $this->decoratedMethodIndex = null;
  76. $this->decoratedMethodArgumentIndex = null;
  77. $this->typesClone = null;
  78. }
  79. }
  80. /**
  81. * {@inheritdoc}
  82. */
  83. protected function processValue($value, bool $isRoot = false)
  84. {
  85. try {
  86. return $this->doProcessValue($value, $isRoot);
  87. } catch (AutowiringFailedException $e) {
  88. if ($this->throwOnAutowiringException) {
  89. throw $e;
  90. }
  91. $this->container->getDefinition($this->currentId)->addError($e->getMessageCallback() ?? $e->getMessage());
  92. return parent::processValue($value, $isRoot);
  93. }
  94. }
  95. /**
  96. * @return mixed
  97. */
  98. private function doProcessValue($value, bool $isRoot = false)
  99. {
  100. if ($value instanceof TypedReference) {
  101. if ($ref = $this->getAutowiredReference($value, true)) {
  102. return $ref;
  103. }
  104. if (ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) {
  105. $message = $this->createTypeNotFoundMessageCallback($value, 'it');
  106. // since the error message varies by referenced id and $this->currentId, so should the id of the dummy errored definition
  107. $this->container->register($id = sprintf('.errored.%s.%s', $this->currentId, (string) $value), $value->getType())
  108. ->addError($message);
  109. return new TypedReference($id, $value->getType(), $value->getInvalidBehavior(), $value->getName());
  110. }
  111. }
  112. $value = parent::processValue($value, $isRoot);
  113. if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
  114. return $value;
  115. }
  116. if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
  117. $this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass()));
  118. return $value;
  119. }
  120. $this->methodCalls = $value->getMethodCalls();
  121. try {
  122. $constructor = $this->getConstructor($value, false);
  123. } catch (RuntimeException $e) {
  124. throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e);
  125. }
  126. if ($constructor) {
  127. array_unshift($this->methodCalls, [$constructor, $value->getArguments()]);
  128. }
  129. $checkAttributes = 80000 <= \PHP_VERSION_ID && !$value->hasTag('container.ignore_attributes');
  130. $this->methodCalls = $this->autowireCalls($reflectionClass, $isRoot, $checkAttributes);
  131. if ($constructor) {
  132. [, $arguments] = array_shift($this->methodCalls);
  133. if ($arguments !== $value->getArguments()) {
  134. $value->setArguments($arguments);
  135. }
  136. }
  137. if ($this->methodCalls !== $value->getMethodCalls()) {
  138. $value->setMethodCalls($this->methodCalls);
  139. }
  140. return $value;
  141. }
  142. private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array
  143. {
  144. $this->decoratedId = null;
  145. $this->decoratedClass = null;
  146. $this->getPreviousValue = null;
  147. if ($isRoot && ($definition = $this->container->getDefinition($this->currentId)) && null !== ($this->decoratedId = $definition->innerServiceId) && $this->container->has($this->decoratedId)) {
  148. $this->decoratedClass = $this->container->findDefinition($this->decoratedId)->getClass();
  149. }
  150. $patchedIndexes = [];
  151. foreach ($this->methodCalls as $i => $call) {
  152. [$method, $arguments] = $call;
  153. if ($method instanceof \ReflectionFunctionAbstract) {
  154. $reflectionMethod = $method;
  155. } else {
  156. $definition = new Definition($reflectionClass->name);
  157. try {
  158. $reflectionMethod = $this->getReflectionMethod($definition, $method);
  159. } catch (RuntimeException $e) {
  160. if ($definition->getFactory()) {
  161. continue;
  162. }
  163. throw $e;
  164. }
  165. }
  166. $arguments = $this->autowireMethod($reflectionMethod, $arguments, $checkAttributes, $i);
  167. if ($arguments !== $call[1]) {
  168. $this->methodCalls[$i][1] = $arguments;
  169. $patchedIndexes[] = $i;
  170. }
  171. }
  172. // use named arguments to skip complex default values
  173. foreach ($patchedIndexes as $i) {
  174. $namedArguments = null;
  175. $arguments = $this->methodCalls[$i][1];
  176. foreach ($arguments as $j => $value) {
  177. if ($namedArguments && !$value instanceof $this->defaultArgument) {
  178. unset($arguments[$j]);
  179. $arguments[$namedArguments[$j]] = $value;
  180. }
  181. if (!$value instanceof $this->defaultArgument) {
  182. continue;
  183. }
  184. if (\PHP_VERSION_ID >= 80100 && (\is_array($value->value) ? $value->value : \is_object($value->value))) {
  185. $namedArguments = $value->names;
  186. }
  187. if ($namedArguments) {
  188. unset($arguments[$j]);
  189. } else {
  190. $arguments[$j] = $value->value;
  191. }
  192. }
  193. $this->methodCalls[$i][1] = $arguments;
  194. }
  195. return $this->methodCalls;
  196. }
  197. /**
  198. * Autowires the constructor or a method.
  199. *
  200. * @throws AutowiringFailedException
  201. */
  202. private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments, bool $checkAttributes, int $methodIndex): array
  203. {
  204. $class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId;
  205. $method = $reflectionMethod->name;
  206. $parameters = $reflectionMethod->getParameters();
  207. if ($reflectionMethod->isVariadic()) {
  208. array_pop($parameters);
  209. }
  210. $this->defaultArgument->names = new \ArrayObject();
  211. foreach ($parameters as $index => $parameter) {
  212. $this->defaultArgument->names[$index] = $parameter->name;
  213. if (\array_key_exists($parameter->name, $arguments)) {
  214. $arguments[$index] = $arguments[$parameter->name];
  215. unset($arguments[$parameter->name]);
  216. }
  217. if (\array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
  218. continue;
  219. }
  220. $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
  221. if ($checkAttributes) {
  222. foreach ($parameter->getAttributes() as $attribute) {
  223. if (TaggedIterator::class === $attribute->getName()) {
  224. $attribute = $attribute->newInstance();
  225. $arguments[$index] = new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, false, $attribute->defaultPriorityMethod);
  226. break;
  227. }
  228. if (TaggedLocator::class === $attribute->getName()) {
  229. $attribute = $attribute->newInstance();
  230. $arguments[$index] = new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, true, $attribute->defaultPriorityMethod));
  231. break;
  232. }
  233. }
  234. if ('' !== ($arguments[$index] ?? '')) {
  235. continue;
  236. }
  237. }
  238. if (!$type) {
  239. if (isset($arguments[$index])) {
  240. continue;
  241. }
  242. // no default value? Then fail
  243. if (!$parameter->isDefaultValueAvailable()) {
  244. // For core classes, isDefaultValueAvailable() can
  245. // be false when isOptional() returns true. If the
  246. // argument *is* optional, allow it to be missing
  247. if ($parameter->isOptional()) {
  248. --$index;
  249. break;
  250. }
  251. $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, false);
  252. $type = $type ? sprintf('is type-hinted "%s"', ltrim($type, '\\')) : 'has no type-hint';
  253. 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));
  254. }
  255. // specifically pass the default value
  256. $arguments[$index] = $this->defaultArgument->withValue($parameter);
  257. continue;
  258. }
  259. $getValue = function () use ($type, $parameter, $class, $method) {
  260. if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, Target::parseName($parameter)), true)) {
  261. $failureMessage = $this->createTypeNotFoundMessageCallback($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
  262. if ($parameter->isDefaultValueAvailable()) {
  263. $value = $this->defaultArgument->withValue($parameter);
  264. } elseif (!$parameter->allowsNull()) {
  265. throw new AutowiringFailedException($this->currentId, $failureMessage);
  266. }
  267. }
  268. return $value;
  269. };
  270. if ($this->decoratedClass && $isDecorated = is_a($this->decoratedClass, $type, true)) {
  271. if ($this->getPreviousValue) {
  272. // The inner service is injected only if there is only 1 argument matching the type of the decorated class
  273. // across all arguments of all autowired methods.
  274. // If a second matching argument is found, the default behavior is restored.
  275. $getPreviousValue = $this->getPreviousValue;
  276. $this->methodCalls[$this->decoratedMethodIndex][1][$this->decoratedMethodArgumentIndex] = $getPreviousValue();
  277. $this->decoratedClass = null; // Prevent further checks
  278. } else {
  279. $arguments[$index] = new TypedReference($this->decoratedId, $this->decoratedClass);
  280. $this->getPreviousValue = $getValue;
  281. $this->decoratedMethodIndex = $methodIndex;
  282. $this->decoratedMethodArgumentIndex = $index;
  283. continue;
  284. }
  285. }
  286. $arguments[$index] = $getValue();
  287. }
  288. if ($parameters && !isset($arguments[++$index])) {
  289. while (0 <= --$index) {
  290. if (!$arguments[$index] instanceof $this->defaultArgument) {
  291. break;
  292. }
  293. unset($arguments[$index]);
  294. }
  295. }
  296. // it's possible index 1 was set, then index 0, then 2, etc
  297. // make sure that we re-order so they're injected as expected
  298. ksort($arguments, \SORT_NATURAL);
  299. return $arguments;
  300. }
  301. /**
  302. * Returns a reference to the service matching the given type, if any.
  303. */
  304. private function getAutowiredReference(TypedReference $reference, bool $filterType): ?TypedReference
  305. {
  306. $this->lastFailure = null;
  307. $type = $reference->getType();
  308. if ($type !== (string) $reference) {
  309. return $reference;
  310. }
  311. if ($filterType && false !== $m = strpbrk($type, '&|')) {
  312. $types = array_diff(explode($m[0], $type), ['int', 'string', 'array', 'bool', 'float', 'iterable', 'object', 'callable', 'null']);
  313. sort($types);
  314. $type = implode($m[0], $types);
  315. }
  316. if (null !== $name = $reference->getName()) {
  317. if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) {
  318. return new TypedReference($alias, $type, $reference->getInvalidBehavior());
  319. }
  320. if (null !== ($alias = $this->getCombinedAlias($type, $name) ?? null) && !$this->container->findDefinition($alias)->isAbstract()) {
  321. return new TypedReference($alias, $type, $reference->getInvalidBehavior());
  322. }
  323. if ($this->container->has($name) && !$this->container->findDefinition($name)->isAbstract()) {
  324. foreach ($this->container->getAliases() as $id => $alias) {
  325. if ($name === (string) $alias && str_starts_with($id, $type.' $')) {
  326. return new TypedReference($name, $type, $reference->getInvalidBehavior());
  327. }
  328. }
  329. }
  330. }
  331. if ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract()) {
  332. return new TypedReference($type, $type, $reference->getInvalidBehavior());
  333. }
  334. if (null !== ($alias = $this->getCombinedAlias($type) ?? null) && !$this->container->findDefinition($alias)->isAbstract()) {
  335. return new TypedReference($alias, $type, $reference->getInvalidBehavior());
  336. }
  337. return null;
  338. }
  339. /**
  340. * Populates the list of available types.
  341. */
  342. private function populateAvailableTypes(ContainerBuilder $container)
  343. {
  344. $this->types = [];
  345. $this->ambiguousServiceTypes = [];
  346. $this->autowiringAliases = [];
  347. foreach ($container->getDefinitions() as $id => $definition) {
  348. $this->populateAvailableType($container, $id, $definition);
  349. }
  350. foreach ($container->getAliases() as $id => $alias) {
  351. $this->populateAutowiringAlias($id);
  352. }
  353. }
  354. /**
  355. * Populates the list of available types for a given definition.
  356. */
  357. private function populateAvailableType(ContainerBuilder $container, string $id, Definition $definition)
  358. {
  359. // Never use abstract services
  360. if ($definition->isAbstract()) {
  361. return;
  362. }
  363. if ('' === $id || '.' === $id[0] || $definition->isDeprecated() || !$reflectionClass = $container->getReflectionClass($definition->getClass(), false)) {
  364. return;
  365. }
  366. foreach ($reflectionClass->getInterfaces() as $reflectionInterface) {
  367. $this->set($reflectionInterface->name, $id);
  368. }
  369. do {
  370. $this->set($reflectionClass->name, $id);
  371. } while ($reflectionClass = $reflectionClass->getParentClass());
  372. $this->populateAutowiringAlias($id);
  373. }
  374. /**
  375. * Associates a type and a service id if applicable.
  376. */
  377. private function set(string $type, string $id)
  378. {
  379. // is this already a type/class that is known to match multiple services?
  380. if (isset($this->ambiguousServiceTypes[$type])) {
  381. $this->ambiguousServiceTypes[$type][] = $id;
  382. return;
  383. }
  384. // check to make sure the type doesn't match multiple services
  385. if (!isset($this->types[$type]) || $this->types[$type] === $id) {
  386. $this->types[$type] = $id;
  387. return;
  388. }
  389. // keep an array of all services matching this type
  390. if (!isset($this->ambiguousServiceTypes[$type])) {
  391. $this->ambiguousServiceTypes[$type] = [$this->types[$type]];
  392. unset($this->types[$type]);
  393. }
  394. $this->ambiguousServiceTypes[$type][] = $id;
  395. }
  396. private function createTypeNotFoundMessageCallback(TypedReference $reference, string $label): \Closure
  397. {
  398. if (null === $this->typesClone->container) {
  399. $this->typesClone->container = new ContainerBuilder($this->container->getParameterBag());
  400. $this->typesClone->container->setAliases($this->container->getAliases());
  401. $this->typesClone->container->setDefinitions($this->container->getDefinitions());
  402. $this->typesClone->container->setResourceTracking(false);
  403. }
  404. $currentId = $this->currentId;
  405. return (function () use ($reference, $label, $currentId) {
  406. return $this->createTypeNotFoundMessage($reference, $label, $currentId);
  407. })->bindTo($this->typesClone);
  408. }
  409. private function createTypeNotFoundMessage(TypedReference $reference, string $label, string $currentId): string
  410. {
  411. if (!$r = $this->container->getReflectionClass($type = $reference->getType(), false)) {
  412. // either $type does not exist or a parent class does not exist
  413. try {
  414. $resource = new ClassExistenceResource($type, false);
  415. // isFresh() will explode ONLY if a parent class/trait does not exist
  416. $resource->isFresh(0);
  417. $parentMsg = false;
  418. } catch (\ReflectionException $e) {
  419. $parentMsg = $e->getMessage();
  420. }
  421. $message = sprintf('has type "%s" but this class %s.', $type, $parentMsg ? sprintf('is missing a parent class (%s)', $parentMsg) : 'was not found');
  422. } else {
  423. $alternatives = $this->createTypeAlternatives($this->container, $reference);
  424. $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists';
  425. $message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $alternatives);
  426. if ($r->isInterface() && !$alternatives) {
  427. $message .= ' Did you create a class that implements this interface?';
  428. }
  429. }
  430. $message = sprintf('Cannot autowire service "%s": %s %s', $currentId, $label, $message);
  431. if (null !== $this->lastFailure) {
  432. $message = $this->lastFailure."\n".$message;
  433. $this->lastFailure = null;
  434. }
  435. return $message;
  436. }
  437. private function createTypeAlternatives(ContainerBuilder $container, TypedReference $reference): string
  438. {
  439. // try suggesting available aliases first
  440. if ($message = $this->getAliasesSuggestionForType($container, $type = $reference->getType())) {
  441. return ' '.$message;
  442. }
  443. if (null === $this->ambiguousServiceTypes) {
  444. $this->populateAvailableTypes($container);
  445. }
  446. $servicesAndAliases = $container->getServiceIds();
  447. if (null !== ($autowiringAliases = $this->autowiringAliases[$type] ?? null) && !isset($autowiringAliases[''])) {
  448. return sprintf(' Available autowiring aliases for this %s are: "$%s".', class_exists($type, false) ? 'class' : 'interface', implode('", "$', $autowiringAliases));
  449. }
  450. if (!$container->has($type) && false !== $key = array_search(strtolower($type), array_map('strtolower', $servicesAndAliases))) {
  451. return sprintf(' Did you mean "%s"?', $servicesAndAliases[$key]);
  452. } elseif (isset($this->ambiguousServiceTypes[$type])) {
  453. $message = sprintf('one of these existing services: "%s"', implode('", "', $this->ambiguousServiceTypes[$type]));
  454. } elseif (isset($this->types[$type])) {
  455. $message = sprintf('the existing "%s" service', $this->types[$type]);
  456. } else {
  457. return '';
  458. }
  459. return sprintf(' You should maybe alias this %s to %s.', class_exists($type, false) ? 'class' : 'interface', $message);
  460. }
  461. private function getAliasesSuggestionForType(ContainerBuilder $container, string $type): ?string
  462. {
  463. $aliases = [];
  464. foreach (class_parents($type) + class_implements($type) as $parent) {
  465. if ($container->has($parent) && !$container->findDefinition($parent)->isAbstract()) {
  466. $aliases[] = $parent;
  467. }
  468. }
  469. if (1 < $len = \count($aliases)) {
  470. $message = 'Try changing the type-hint to one of its parents: ';
  471. for ($i = 0, --$len; $i < $len; ++$i) {
  472. $message .= sprintf('%s "%s", ', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);
  473. }
  474. $message .= sprintf('or %s "%s".', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);
  475. return $message;
  476. }
  477. if ($aliases) {
  478. return sprintf('Try changing the type-hint to "%s" instead.', $aliases[0]);
  479. }
  480. return null;
  481. }
  482. private function populateAutowiringAlias(string $id): void
  483. {
  484. if (!preg_match('/(?(DEFINE)(?<V>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))^((?&V)(?:\\\\(?&V))*+)(?: \$((?&V)))?$/', $id, $m)) {
  485. return;
  486. }
  487. $type = $m[2];
  488. $name = $m[3] ?? '';
  489. if (class_exists($type, false) || interface_exists($type, false)) {
  490. $this->autowiringAliases[$type][$name] = $name;
  491. }
  492. }
  493. private function getCombinedAlias(string $type, ?string $name = null): ?string
  494. {
  495. if (str_contains($type, '&')) {
  496. $types = explode('&', $type);
  497. } elseif (str_contains($type, '|')) {
  498. $types = explode('|', $type);
  499. } else {
  500. return null;
  501. }
  502. $alias = null;
  503. $suffix = $name ? ' $'.$name : '';
  504. foreach ($types as $type) {
  505. if (!$this->container->hasAlias($type.$suffix)) {
  506. return null;
  507. }
  508. if (null === $alias) {
  509. $alias = (string) $this->container->getAlias($type.$suffix);
  510. } elseif ((string) $this->container->getAlias($type.$suffix) !== $alias) {
  511. return null;
  512. }
  513. }
  514. return $alias;
  515. }
  516. }