Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 /*
|
Chris@0
|
4 * This file is part of the Symfony package.
|
Chris@0
|
5 *
|
Chris@0
|
6 * (c) Fabien Potencier <fabien@symfony.com>
|
Chris@0
|
7 *
|
Chris@0
|
8 * For the full copyright and license information, please view the LICENSE
|
Chris@0
|
9 * file that was distributed with this source code.
|
Chris@0
|
10 */
|
Chris@0
|
11
|
Chris@0
|
12 namespace Symfony\Component\DependencyInjection\Compiler;
|
Chris@0
|
13
|
Chris@0
|
14 use Symfony\Component\DependencyInjection\Config\AutowireServiceResource;
|
Chris@0
|
15 use Symfony\Component\DependencyInjection\ContainerBuilder;
|
Chris@0
|
16 use Symfony\Component\DependencyInjection\Definition;
|
Chris@0
|
17 use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
Chris@0
|
18 use Symfony\Component\DependencyInjection\Reference;
|
Chris@0
|
19
|
Chris@0
|
20 /**
|
Chris@0
|
21 * Guesses constructor arguments of services definitions and try to instantiate services if necessary.
|
Chris@0
|
22 *
|
Chris@0
|
23 * @author Kévin Dunglas <dunglas@gmail.com>
|
Chris@0
|
24 */
|
Chris@0
|
25 class AutowirePass implements CompilerPassInterface
|
Chris@0
|
26 {
|
Chris@0
|
27 private $container;
|
Chris@0
|
28 private $reflectionClasses = array();
|
Chris@0
|
29 private $definedTypes = array();
|
Chris@0
|
30 private $types;
|
Chris@0
|
31 private $ambiguousServiceTypes = array();
|
Chris@0
|
32 private $autowired = array();
|
Chris@0
|
33
|
Chris@0
|
34 /**
|
Chris@0
|
35 * {@inheritdoc}
|
Chris@0
|
36 */
|
Chris@0
|
37 public function process(ContainerBuilder $container)
|
Chris@0
|
38 {
|
Chris@0
|
39 $throwingAutoloader = function ($class) { throw new \ReflectionException(sprintf('Class %s does not exist', $class)); };
|
Chris@0
|
40 spl_autoload_register($throwingAutoloader);
|
Chris@0
|
41
|
Chris@0
|
42 try {
|
Chris@0
|
43 $this->container = $container;
|
Chris@0
|
44 foreach ($container->getDefinitions() as $id => $definition) {
|
Chris@0
|
45 if ($definition->isAutowired()) {
|
Chris@0
|
46 $this->completeDefinition($id, $definition);
|
Chris@0
|
47 }
|
Chris@0
|
48 }
|
Chris@0
|
49 } finally {
|
Chris@0
|
50 spl_autoload_unregister($throwingAutoloader);
|
Chris@0
|
51
|
Chris@0
|
52 // Free memory and remove circular reference to container
|
Chris@0
|
53 $this->reflectionClasses = array();
|
Chris@0
|
54 $this->definedTypes = array();
|
Chris@0
|
55 $this->types = null;
|
Chris@0
|
56 $this->ambiguousServiceTypes = array();
|
Chris@0
|
57 $this->autowired = array();
|
Chris@0
|
58 }
|
Chris@0
|
59 }
|
Chris@0
|
60
|
Chris@0
|
61 /**
|
Chris@0
|
62 * Creates a resource to help know if this service has changed.
|
Chris@0
|
63 *
|
Chris@0
|
64 * @param \ReflectionClass $reflectionClass
|
Chris@0
|
65 *
|
Chris@0
|
66 * @return AutowireServiceResource
|
Chris@0
|
67 */
|
Chris@0
|
68 public static function createResourceForClass(\ReflectionClass $reflectionClass)
|
Chris@0
|
69 {
|
Chris@0
|
70 $metadata = array();
|
Chris@0
|
71
|
Chris@0
|
72 if ($constructor = $reflectionClass->getConstructor()) {
|
Chris@0
|
73 $metadata['__construct'] = self::getResourceMetadataForMethod($constructor);
|
Chris@0
|
74 }
|
Chris@0
|
75
|
Chris@0
|
76 foreach (self::getSetters($reflectionClass) as $reflectionMethod) {
|
Chris@0
|
77 $metadata[$reflectionMethod->name] = self::getResourceMetadataForMethod($reflectionMethod);
|
Chris@0
|
78 }
|
Chris@0
|
79
|
Chris@0
|
80 return new AutowireServiceResource($reflectionClass->name, $reflectionClass->getFileName(), $metadata);
|
Chris@0
|
81 }
|
Chris@0
|
82
|
Chris@0
|
83 /**
|
Chris@0
|
84 * Wires the given definition.
|
Chris@0
|
85 *
|
Chris@0
|
86 * @param string $id
|
Chris@0
|
87 * @param Definition $definition
|
Chris@0
|
88 *
|
Chris@0
|
89 * @throws RuntimeException
|
Chris@0
|
90 */
|
Chris@0
|
91 private function completeDefinition($id, Definition $definition)
|
Chris@0
|
92 {
|
Chris@0
|
93 if ($definition->getFactory()) {
|
Chris@0
|
94 throw new RuntimeException(sprintf('Service "%s" can use either autowiring or a factory, not both.', $id));
|
Chris@0
|
95 }
|
Chris@0
|
96
|
Chris@0
|
97 if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
|
Chris@0
|
98 return;
|
Chris@0
|
99 }
|
Chris@0
|
100
|
Chris@0
|
101 if ($this->container->isTrackingResources()) {
|
Chris@0
|
102 $this->container->addResource(static::createResourceForClass($reflectionClass));
|
Chris@0
|
103 }
|
Chris@0
|
104
|
Chris@0
|
105 if (!$constructor = $reflectionClass->getConstructor()) {
|
Chris@0
|
106 return;
|
Chris@0
|
107 }
|
Chris@0
|
108 $parameters = $constructor->getParameters();
|
Chris@0
|
109 if (method_exists('ReflectionMethod', 'isVariadic') && $constructor->isVariadic()) {
|
Chris@0
|
110 array_pop($parameters);
|
Chris@0
|
111 }
|
Chris@0
|
112
|
Chris@0
|
113 $arguments = $definition->getArguments();
|
Chris@0
|
114 foreach ($parameters as $index => $parameter) {
|
Chris@0
|
115 if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
|
Chris@0
|
116 continue;
|
Chris@0
|
117 }
|
Chris@0
|
118
|
Chris@0
|
119 try {
|
Chris@0
|
120 if (!$typeHint = $parameter->getClass()) {
|
Chris@0
|
121 if (isset($arguments[$index])) {
|
Chris@0
|
122 continue;
|
Chris@0
|
123 }
|
Chris@0
|
124
|
Chris@0
|
125 // no default value? Then fail
|
Chris@0
|
126 if (!$parameter->isOptional()) {
|
Chris@0
|
127 throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id));
|
Chris@0
|
128 }
|
Chris@0
|
129
|
Chris@0
|
130 // specifically pass the default value
|
Chris@0
|
131 $arguments[$index] = $parameter->getDefaultValue();
|
Chris@0
|
132
|
Chris@0
|
133 continue;
|
Chris@0
|
134 }
|
Chris@0
|
135
|
Chris@0
|
136 if (isset($this->autowired[$typeHint->name])) {
|
Chris@0
|
137 $arguments[$index] = $this->autowired[$typeHint->name] ? new Reference($this->autowired[$typeHint->name]) : null;
|
Chris@0
|
138 continue;
|
Chris@0
|
139 }
|
Chris@0
|
140
|
Chris@0
|
141 if (null === $this->types) {
|
Chris@0
|
142 $this->populateAvailableTypes();
|
Chris@0
|
143 }
|
Chris@0
|
144
|
Chris@0
|
145 if (isset($this->types[$typeHint->name])) {
|
Chris@0
|
146 $value = new Reference($this->types[$typeHint->name]);
|
Chris@0
|
147 } else {
|
Chris@0
|
148 try {
|
Chris@0
|
149 $value = $this->createAutowiredDefinition($typeHint, $id);
|
Chris@0
|
150 } catch (RuntimeException $e) {
|
Chris@0
|
151 if ($parameter->isDefaultValueAvailable()) {
|
Chris@0
|
152 $value = $parameter->getDefaultValue();
|
Chris@0
|
153 } elseif ($parameter->allowsNull()) {
|
Chris@0
|
154 $value = null;
|
Chris@0
|
155 } else {
|
Chris@0
|
156 throw $e;
|
Chris@0
|
157 }
|
Chris@0
|
158 $this->autowired[$typeHint->name] = false;
|
Chris@0
|
159 }
|
Chris@0
|
160 }
|
Chris@0
|
161 } catch (\ReflectionException $e) {
|
Chris@0
|
162 // Typehint against a non-existing class
|
Chris@0
|
163
|
Chris@0
|
164 if (!$parameter->isDefaultValueAvailable()) {
|
Chris@0
|
165 throw new RuntimeException(sprintf('Cannot autowire argument %s for %s because the type-hinted class does not exist (%s).', $index + 1, $definition->getClass(), $e->getMessage()), 0, $e);
|
Chris@0
|
166 }
|
Chris@0
|
167
|
Chris@0
|
168 $value = $parameter->getDefaultValue();
|
Chris@0
|
169 }
|
Chris@0
|
170
|
Chris@0
|
171 $arguments[$index] = $value;
|
Chris@0
|
172 }
|
Chris@0
|
173
|
Chris@0
|
174 if ($parameters && !isset($arguments[++$index])) {
|
Chris@0
|
175 while (0 <= --$index) {
|
Chris@0
|
176 $parameter = $parameters[$index];
|
Chris@0
|
177 if (!$parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== $arguments[$index]) {
|
Chris@0
|
178 break;
|
Chris@0
|
179 }
|
Chris@0
|
180 unset($arguments[$index]);
|
Chris@0
|
181 }
|
Chris@0
|
182 }
|
Chris@0
|
183
|
Chris@0
|
184 // it's possible index 1 was set, then index 0, then 2, etc
|
Chris@0
|
185 // make sure that we re-order so they're injected as expected
|
Chris@0
|
186 ksort($arguments);
|
Chris@0
|
187 $definition->setArguments($arguments);
|
Chris@0
|
188 }
|
Chris@0
|
189
|
Chris@0
|
190 /**
|
Chris@0
|
191 * Populates the list of available types.
|
Chris@0
|
192 */
|
Chris@0
|
193 private function populateAvailableTypes()
|
Chris@0
|
194 {
|
Chris@0
|
195 $this->types = array();
|
Chris@0
|
196
|
Chris@0
|
197 foreach ($this->container->getDefinitions() as $id => $definition) {
|
Chris@0
|
198 $this->populateAvailableType($id, $definition);
|
Chris@0
|
199 }
|
Chris@0
|
200 }
|
Chris@0
|
201
|
Chris@0
|
202 /**
|
Chris@0
|
203 * Populates the list of available types for a given definition.
|
Chris@0
|
204 *
|
Chris@0
|
205 * @param string $id
|
Chris@0
|
206 * @param Definition $definition
|
Chris@0
|
207 */
|
Chris@0
|
208 private function populateAvailableType($id, Definition $definition)
|
Chris@0
|
209 {
|
Chris@0
|
210 // Never use abstract services
|
Chris@0
|
211 if ($definition->isAbstract()) {
|
Chris@0
|
212 return;
|
Chris@0
|
213 }
|
Chris@0
|
214
|
Chris@0
|
215 foreach ($definition->getAutowiringTypes() as $type) {
|
Chris@0
|
216 $this->definedTypes[$type] = true;
|
Chris@0
|
217 $this->types[$type] = $id;
|
Chris@0
|
218 unset($this->ambiguousServiceTypes[$type]);
|
Chris@0
|
219 }
|
Chris@0
|
220
|
Chris@0
|
221 if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
|
Chris@0
|
222 return;
|
Chris@0
|
223 }
|
Chris@0
|
224
|
Chris@0
|
225 foreach ($reflectionClass->getInterfaces() as $reflectionInterface) {
|
Chris@0
|
226 $this->set($reflectionInterface->name, $id);
|
Chris@0
|
227 }
|
Chris@0
|
228
|
Chris@0
|
229 do {
|
Chris@0
|
230 $this->set($reflectionClass->name, $id);
|
Chris@0
|
231 } while ($reflectionClass = $reflectionClass->getParentClass());
|
Chris@0
|
232 }
|
Chris@0
|
233
|
Chris@0
|
234 /**
|
Chris@0
|
235 * Associates a type and a service id if applicable.
|
Chris@0
|
236 *
|
Chris@0
|
237 * @param string $type
|
Chris@0
|
238 * @param string $id
|
Chris@0
|
239 */
|
Chris@0
|
240 private function set($type, $id)
|
Chris@0
|
241 {
|
Chris@0
|
242 if (isset($this->definedTypes[$type])) {
|
Chris@0
|
243 return;
|
Chris@0
|
244 }
|
Chris@0
|
245
|
Chris@0
|
246 // is this already a type/class that is known to match multiple services?
|
Chris@0
|
247 if (isset($this->ambiguousServiceTypes[$type])) {
|
Chris@0
|
248 $this->ambiguousServiceTypes[$type][] = $id;
|
Chris@0
|
249
|
Chris@0
|
250 return;
|
Chris@0
|
251 }
|
Chris@0
|
252
|
Chris@0
|
253 // check to make sure the type doesn't match multiple services
|
Chris@0
|
254 if (!isset($this->types[$type]) || $this->types[$type] === $id) {
|
Chris@0
|
255 $this->types[$type] = $id;
|
Chris@0
|
256
|
Chris@0
|
257 return;
|
Chris@0
|
258 }
|
Chris@0
|
259
|
Chris@0
|
260 // keep an array of all services matching this type
|
Chris@0
|
261 if (!isset($this->ambiguousServiceTypes[$type])) {
|
Chris@0
|
262 $this->ambiguousServiceTypes[$type] = array($this->types[$type]);
|
Chris@0
|
263 unset($this->types[$type]);
|
Chris@0
|
264 }
|
Chris@0
|
265 $this->ambiguousServiceTypes[$type][] = $id;
|
Chris@0
|
266 }
|
Chris@0
|
267
|
Chris@0
|
268 /**
|
Chris@0
|
269 * Registers a definition for the type if possible or throws an exception.
|
Chris@0
|
270 *
|
Chris@0
|
271 * @param \ReflectionClass $typeHint
|
Chris@0
|
272 * @param string $id
|
Chris@0
|
273 *
|
Chris@0
|
274 * @return Reference A reference to the registered definition
|
Chris@0
|
275 *
|
Chris@0
|
276 * @throws RuntimeException
|
Chris@0
|
277 */
|
Chris@0
|
278 private function createAutowiredDefinition(\ReflectionClass $typeHint, $id)
|
Chris@0
|
279 {
|
Chris@0
|
280 if (isset($this->ambiguousServiceTypes[$typeHint->name])) {
|
Chris@0
|
281 $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
|
Chris@0
|
282 $matchingServices = implode(', ', $this->ambiguousServiceTypes[$typeHint->name]);
|
Chris@0
|
283
|
Chris@0
|
284 throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $typeHint->name, $id, $classOrInterface, $matchingServices));
|
Chris@0
|
285 }
|
Chris@0
|
286
|
Chris@0
|
287 if (!$typeHint->isInstantiable()) {
|
Chris@0
|
288 $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
|
Chris@0
|
289 throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface));
|
Chris@0
|
290 }
|
Chris@0
|
291
|
Chris@0
|
292 $this->autowired[$typeHint->name] = $argumentId = sprintf('autowired.%s', $typeHint->name);
|
Chris@0
|
293
|
Chris@0
|
294 $argumentDefinition = $this->container->register($argumentId, $typeHint->name);
|
Chris@0
|
295 $argumentDefinition->setPublic(false);
|
Chris@0
|
296
|
Chris@0
|
297 try {
|
Chris@0
|
298 $this->completeDefinition($argumentId, $argumentDefinition);
|
Chris@0
|
299 } catch (RuntimeException $e) {
|
Chris@0
|
300 $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
|
Chris@0
|
301 $message = sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface);
|
Chris@0
|
302 throw new RuntimeException($message, 0, $e);
|
Chris@0
|
303 }
|
Chris@0
|
304
|
Chris@0
|
305 return new Reference($argumentId);
|
Chris@0
|
306 }
|
Chris@0
|
307
|
Chris@0
|
308 /**
|
Chris@0
|
309 * Retrieves the reflection class associated with the given service.
|
Chris@0
|
310 *
|
Chris@0
|
311 * @param string $id
|
Chris@0
|
312 * @param Definition $definition
|
Chris@0
|
313 *
|
Chris@0
|
314 * @return \ReflectionClass|false
|
Chris@0
|
315 */
|
Chris@0
|
316 private function getReflectionClass($id, Definition $definition)
|
Chris@0
|
317 {
|
Chris@0
|
318 if (isset($this->reflectionClasses[$id])) {
|
Chris@0
|
319 return $this->reflectionClasses[$id];
|
Chris@0
|
320 }
|
Chris@0
|
321
|
Chris@0
|
322 // Cannot use reflection if the class isn't set
|
Chris@0
|
323 if (!$class = $definition->getClass()) {
|
Chris@0
|
324 return false;
|
Chris@0
|
325 }
|
Chris@0
|
326
|
Chris@0
|
327 $class = $this->container->getParameterBag()->resolveValue($class);
|
Chris@0
|
328
|
Chris@0
|
329 if ($deprecated = $definition->isDeprecated()) {
|
Chris@0
|
330 $prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) {
|
Chris@0
|
331 return (E_USER_DEPRECATED === $level || !$prevErrorHandler) ? false : $prevErrorHandler($level, $message, $file, $line);
|
Chris@0
|
332 });
|
Chris@0
|
333 }
|
Chris@0
|
334
|
Chris@0
|
335 $e = null;
|
Chris@0
|
336
|
Chris@0
|
337 try {
|
Chris@0
|
338 $reflector = new \ReflectionClass($class);
|
Chris@0
|
339 } catch (\Exception $e) {
|
Chris@0
|
340 } catch (\Throwable $e) {
|
Chris@0
|
341 }
|
Chris@0
|
342
|
Chris@0
|
343 if ($deprecated) {
|
Chris@0
|
344 restore_error_handler();
|
Chris@0
|
345 }
|
Chris@0
|
346
|
Chris@0
|
347 if (null !== $e) {
|
Chris@0
|
348 if (!$e instanceof \ReflectionException) {
|
Chris@0
|
349 throw $e;
|
Chris@0
|
350 }
|
Chris@0
|
351 $reflector = false;
|
Chris@0
|
352 }
|
Chris@0
|
353
|
Chris@0
|
354 return $this->reflectionClasses[$id] = $reflector;
|
Chris@0
|
355 }
|
Chris@0
|
356
|
Chris@0
|
357 /**
|
Chris@0
|
358 * @param \ReflectionClass $reflectionClass
|
Chris@0
|
359 *
|
Chris@0
|
360 * @return \ReflectionMethod[]
|
Chris@0
|
361 */
|
Chris@0
|
362 private static function getSetters(\ReflectionClass $reflectionClass)
|
Chris@0
|
363 {
|
Chris@0
|
364 foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
|
Chris@0
|
365 if (!$reflectionMethod->isStatic() && 1 === $reflectionMethod->getNumberOfParameters() && 0 === strpos($reflectionMethod->name, 'set')) {
|
Chris@0
|
366 yield $reflectionMethod;
|
Chris@0
|
367 }
|
Chris@0
|
368 }
|
Chris@0
|
369 }
|
Chris@0
|
370
|
Chris@0
|
371 private static function getResourceMetadataForMethod(\ReflectionMethod $method)
|
Chris@0
|
372 {
|
Chris@0
|
373 $methodArgumentsMetadata = array();
|
Chris@0
|
374 foreach ($method->getParameters() as $parameter) {
|
Chris@0
|
375 try {
|
Chris@0
|
376 $class = $parameter->getClass();
|
Chris@0
|
377 } catch (\ReflectionException $e) {
|
Chris@0
|
378 // type-hint is against a non-existent class
|
Chris@0
|
379 $class = false;
|
Chris@0
|
380 }
|
Chris@0
|
381
|
Chris@0
|
382 $isVariadic = method_exists($parameter, 'isVariadic') && $parameter->isVariadic();
|
Chris@0
|
383 $methodArgumentsMetadata[] = array(
|
Chris@0
|
384 'class' => $class,
|
Chris@0
|
385 'isOptional' => $parameter->isOptional(),
|
Chris@0
|
386 'defaultValue' => ($parameter->isOptional() && !$isVariadic) ? $parameter->getDefaultValue() : null,
|
Chris@0
|
387 );
|
Chris@0
|
388 }
|
Chris@0
|
389
|
Chris@0
|
390 return $methodArgumentsMetadata;
|
Chris@0
|
391 }
|
Chris@0
|
392 }
|