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