ResolveBindingsPass.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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\DependencyInjection\Argument\AbstractArgument;
  12. use Symfony\Component\DependencyInjection\Argument\BoundArgument;
  13. use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
  14. use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
  15. use Symfony\Component\DependencyInjection\Attribute\Target;
  16. use Symfony\Component\DependencyInjection\ContainerBuilder;
  17. use Symfony\Component\DependencyInjection\Definition;
  18. use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
  19. use Symfony\Component\DependencyInjection\Exception\RuntimeException;
  20. use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
  21. use Symfony\Component\DependencyInjection\Reference;
  22. use Symfony\Component\DependencyInjection\TypedReference;
  23. /**
  24. * @author Guilhem Niot <guilhem.niot@gmail.com>
  25. */
  26. class ResolveBindingsPass extends AbstractRecursivePass
  27. {
  28. private $usedBindings = [];
  29. private $unusedBindings = [];
  30. private $errorMessages = [];
  31. /**
  32. * {@inheritdoc}
  33. */
  34. public function process(ContainerBuilder $container)
  35. {
  36. $this->usedBindings = $container->getRemovedBindingIds();
  37. try {
  38. parent::process($container);
  39. foreach ($this->unusedBindings as [$key, $serviceId, $bindingType, $file]) {
  40. $argumentType = $argumentName = $message = null;
  41. if (str_contains($key, ' ')) {
  42. [$argumentType, $argumentName] = explode(' ', $key, 2);
  43. } elseif ('$' === $key[0]) {
  44. $argumentName = $key;
  45. } else {
  46. $argumentType = $key;
  47. }
  48. if ($argumentType) {
  49. $message .= sprintf('of type "%s" ', $argumentType);
  50. }
  51. if ($argumentName) {
  52. $message .= sprintf('named "%s" ', $argumentName);
  53. }
  54. if (BoundArgument::DEFAULTS_BINDING === $bindingType) {
  55. $message .= 'under "_defaults"';
  56. } elseif (BoundArgument::INSTANCEOF_BINDING === $bindingType) {
  57. $message .= 'under "_instanceof"';
  58. } else {
  59. $message .= sprintf('for service "%s"', $serviceId);
  60. }
  61. if ($file) {
  62. $message .= sprintf(' in file "%s"', $file);
  63. }
  64. $message = sprintf('A binding is configured for an argument %s, but no corresponding argument has been found. It may be unused and should be removed, or it may have a typo.', $message);
  65. if ($this->errorMessages) {
  66. $message .= sprintf("\nCould be related to%s:", 1 < \count($this->errorMessages) ? ' one of' : '');
  67. }
  68. foreach ($this->errorMessages as $m) {
  69. $message .= "\n - ".$m;
  70. }
  71. throw new InvalidArgumentException($message);
  72. }
  73. } finally {
  74. $this->usedBindings = [];
  75. $this->unusedBindings = [];
  76. $this->errorMessages = [];
  77. }
  78. }
  79. /**
  80. * {@inheritdoc}
  81. */
  82. protected function processValue($value, bool $isRoot = false)
  83. {
  84. if ($value instanceof TypedReference && $value->getType() === (string) $value) {
  85. // Already checked
  86. $bindings = $this->container->getDefinition($this->currentId)->getBindings();
  87. $name = $value->getName();
  88. if (isset($name, $bindings[$name = $value.' $'.$name])) {
  89. return $this->getBindingValue($bindings[$name]);
  90. }
  91. if (isset($bindings[$value->getType()])) {
  92. return $this->getBindingValue($bindings[$value->getType()]);
  93. }
  94. return parent::processValue($value, $isRoot);
  95. }
  96. if (!$value instanceof Definition || !$bindings = $value->getBindings()) {
  97. return parent::processValue($value, $isRoot);
  98. }
  99. $bindingNames = [];
  100. foreach ($bindings as $key => $binding) {
  101. [$bindingValue, $bindingId, $used, $bindingType, $file] = $binding->getValues();
  102. if ($used) {
  103. $this->usedBindings[$bindingId] = true;
  104. unset($this->unusedBindings[$bindingId]);
  105. } elseif (!isset($this->usedBindings[$bindingId])) {
  106. $this->unusedBindings[$bindingId] = [$key, $this->currentId, $bindingType, $file];
  107. }
  108. if (preg_match('/^(?:(?:array|bool|float|int|string|iterable|([^ $]++)) )\$/', $key, $m)) {
  109. $bindingNames[substr($key, \strlen($m[0]))] = $binding;
  110. }
  111. if (!isset($m[1])) {
  112. continue;
  113. }
  114. if (is_subclass_of($m[1], \UnitEnum::class)) {
  115. $bindingNames[substr($key, \strlen($m[0]))] = $binding;
  116. continue;
  117. }
  118. if (null !== $bindingValue && !$bindingValue instanceof Reference && !$bindingValue instanceof Definition && !$bindingValue instanceof TaggedIteratorArgument && !$bindingValue instanceof ServiceLocatorArgument) {
  119. throw new InvalidArgumentException(sprintf('Invalid value for binding key "%s" for service "%s": expected "%s", "%s", "%s", "%s" or null, "%s" given.', $key, $this->currentId, Reference::class, Definition::class, TaggedIteratorArgument::class, ServiceLocatorArgument::class, get_debug_type($bindingValue)));
  120. }
  121. }
  122. if ($value->isAbstract()) {
  123. return parent::processValue($value, $isRoot);
  124. }
  125. $calls = $value->getMethodCalls();
  126. try {
  127. if ($constructor = $this->getConstructor($value, false)) {
  128. $calls[] = [$constructor, $value->getArguments()];
  129. }
  130. } catch (RuntimeException $e) {
  131. $this->errorMessages[] = $e->getMessage();
  132. $this->container->getDefinition($this->currentId)->addError($e->getMessage());
  133. return parent::processValue($value, $isRoot);
  134. }
  135. foreach ($calls as $i => $call) {
  136. [$method, $arguments] = $call;
  137. if ($method instanceof \ReflectionFunctionAbstract) {
  138. $reflectionMethod = $method;
  139. } else {
  140. try {
  141. $reflectionMethod = $this->getReflectionMethod($value, $method);
  142. } catch (RuntimeException $e) {
  143. if ($value->getFactory()) {
  144. continue;
  145. }
  146. throw $e;
  147. }
  148. }
  149. $names = [];
  150. foreach ($reflectionMethod->getParameters() as $key => $parameter) {
  151. $names[$key] = $parameter->name;
  152. if (\array_key_exists($key, $arguments) && '' !== $arguments[$key] && !$arguments[$key] instanceof AbstractArgument) {
  153. continue;
  154. }
  155. if (\array_key_exists($parameter->name, $arguments) && '' !== $arguments[$parameter->name] && !$arguments[$parameter->name] instanceof AbstractArgument) {
  156. continue;
  157. }
  158. $typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter);
  159. $name = Target::parseName($parameter);
  160. if ($typeHint && \array_key_exists($k = ltrim($typeHint, '\\').' $'.$name, $bindings)) {
  161. $arguments[$key] = $this->getBindingValue($bindings[$k]);
  162. continue;
  163. }
  164. if (\array_key_exists('$'.$name, $bindings)) {
  165. $arguments[$key] = $this->getBindingValue($bindings['$'.$name]);
  166. continue;
  167. }
  168. if ($typeHint && '\\' === $typeHint[0] && isset($bindings[$typeHint = substr($typeHint, 1)])) {
  169. $arguments[$key] = $this->getBindingValue($bindings[$typeHint]);
  170. continue;
  171. }
  172. if (isset($bindingNames[$name]) || isset($bindingNames[$parameter->name])) {
  173. $bindingKey = array_search($binding, $bindings, true);
  174. $argumentType = substr($bindingKey, 0, strpos($bindingKey, ' '));
  175. $this->errorMessages[] = sprintf('Did you forget to add the type "%s" to argument "$%s" of method "%s::%s()"?', $argumentType, $parameter->name, $reflectionMethod->class, $reflectionMethod->name);
  176. }
  177. }
  178. foreach ($names as $key => $name) {
  179. if (\array_key_exists($name, $arguments) && (0 === $key || \array_key_exists($key - 1, $arguments))) {
  180. if (!array_key_exists($key, $arguments)) {
  181. $arguments[$key] = $arguments[$name];
  182. }
  183. unset($arguments[$name]);
  184. }
  185. }
  186. if ($arguments !== $call[1]) {
  187. ksort($arguments, \SORT_NATURAL);
  188. $calls[$i][1] = $arguments;
  189. }
  190. }
  191. if ($constructor) {
  192. [, $arguments] = array_pop($calls);
  193. if ($arguments !== $value->getArguments()) {
  194. $value->setArguments($arguments);
  195. }
  196. }
  197. if ($calls !== $value->getMethodCalls()) {
  198. $value->setMethodCalls($calls);
  199. }
  200. return parent::processValue($value, $isRoot);
  201. }
  202. /**
  203. * @return mixed
  204. */
  205. private function getBindingValue(BoundArgument $binding)
  206. {
  207. [$bindingValue, $bindingId] = $binding->getValues();
  208. $this->usedBindings[$bindingId] = true;
  209. unset($this->unusedBindings[$bindingId]);
  210. return $bindingValue;
  211. }
  212. }