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