Completed
Pull Request — master (#12)
by Jakub
03:54
created

AutowiringCompilerPass::getClassUseStatements()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
namespace Skrz\Bundle\AutowiringBundle\DependencyInjection\Compiler;
3
4
use Doctrine\Common\Annotations\AnnotationReader;
5
use Doctrine\Common\Annotations\AnnotationRegistry;
6
use Doctrine\Common\Annotations\PhpParser;
7
use ReflectionClass;
8
use ReflectionMethod;
9
use ReflectionParameter;
10
use ReflectionProperty;
11
use RuntimeException;
12
use Skrz\Bundle\AutowiringBundle\Annotation\Autowired;
13
use Skrz\Bundle\AutowiringBundle\Annotation\Value;
14
use Skrz\Bundle\AutowiringBundle\DependencyInjection\ClassMultiMap;
15
use Skrz\Bundle\AutowiringBundle\Exception\AutowiringException;
16
use Skrz\Bundle\AutowiringBundle\Exception\MultipleValuesException;
17
use Skrz\Bundle\AutowiringBundle\Exception\NoValueException;
18
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
19
use Symfony\Component\DependencyInjection\ContainerBuilder;
20
use Symfony\Component\DependencyInjection\Definition;
21
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
22
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
23
use Symfony\Component\DependencyInjection\Reference;
24
use function array_merge;
25
26
/**
27
 * @author Jakub Kulhan <[email protected]>
28
 */
29
class AutowiringCompilerPass implements CompilerPassInterface
30
{
31
32
	/** @var ClassMultiMap */
33
	private $classMap;
34
35
	/** @var AnnotationReader */
36
	private $annotationReader;
37
38
	/** @var PhpParser */
39
	private $phpParser;
40
41
	/** @var string[][] */
42
	private $cachedUseStatements = [];
43
44 6
	/** @var ParameterBagInterface */
45
	private $parameterBag;
46 6
47 6
	public function __construct(ClassMultiMap $classMap, AnnotationReader $annotationReader, PhpParser $phpParser)
48 6
	{
49 6
		$this->classMap = $classMap;
50 6
		$this->annotationReader = $annotationReader;
51
		$this->phpParser = $phpParser;
52 5
		AnnotationRegistry::registerFile(__DIR__ . "/../../Annotation/Autowired.php");
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...egistry::registerFile() has been deprecated with message: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists')

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
53
	}
54 5
55
	public function process(ContainerBuilder $container)
56
	{
57 5
		$this->parameterBag = $parameterBag = $container->getParameterBag();
58 5
59 4
		try {
60
			$preferredServices = (array)$parameterBag->resolveValue("%autowiring.preferred_services%");
61
		} catch (ParameterNotFoundException $exception) {
62
			$preferredServices = [];
63
		}
64
65 5
		try {
66 5
			$fastAnnotationChecksRegex = "/" . implode("|", array_map(function ($s) {
67 5
					return preg_quote($s);
68
				}, (array)$parameterBag->resolveValue("%autowiring.fast_annotation_checks%"))) . "/";
69
		} catch (ParameterNotFoundException $exception) {
70 5
			$fastAnnotationChecksRegex = null;
71 5
		}
72
73
		foreach ($container->getDefinitions() as $serviceId => $definition) {
74
			if ($this->canDefinitionBeAutowired($serviceId, $definition) === false) {
75
				continue;
76 5
			}
77 5
78
			try {
79 5
				$className = $parameterBag->resolveValue($definition->getClass());
80 5
				$reflectionClass = $container->getReflectionClass($className, false);
81 5
				if ($reflectionClass === null) {
82 5
					continue;
83 5
				}
84 5
85
				$this->autowireClass(
86 5
					$className,
87
					$reflectionClass,
88
					$definition,
89 4
					$fastAnnotationChecksRegex,
90
					$preferredServices,
91 5
					$parameterBag
92 1
				);
93 1
94 1
				// add files to cache
95
				$container->addObjectResource($reflectionClass);
96 1
97
			} catch (AutowiringException $exception) {
98 4
				throw new AutowiringException(
99 4
					sprintf("%s (service: %s)", $exception->getMessage(), $serviceId),
100
					$exception->getCode(),
101
					$exception
102
				);
103
			}
104
		}
105
	}
106
107
	/**
108
	 * @param string $className
109 5
	 * @param ReflectionClass $reflectionClass
110
	 * @param Definition $definition
111
	 * @param string $fastAnnotationChecksRegex
112
	 * @param string[] $preferredServices
113
	 * @param ParameterBagInterface $parameterBag
114
	 */
115
	private function autowireClass(
116
		string $className,
117
		ReflectionClass $reflectionClass,
118 5
		Definition $definition,
119 4
		?string $fastAnnotationChecksRegex,
120 4
		array $preferredServices,
121 4
		ParameterBagInterface $parameterBag
122 4
	) {
123 4
		// constructor - autowire always
124
		if ($reflectionClass->getConstructor()) {
125 4
			$definition->setArguments(
126 3
				$this->autowireMethod(
127 3
					$className,
128
					$reflectionClass->getConstructor(),
129 4
					$definition->getArguments(),
130
					$preferredServices
131
				)
132 4
			);
133
		}
134 4
135 3
		if ($fastAnnotationChecksRegex === null ||
136 3
			($reflectionClass->getDocComment() &&
137
				preg_match($fastAnnotationChecksRegex, $reflectionClass->getDocComment()))
138
		) {
139
			// method calls @Autowired
140
			foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
141
				if ($reflectionMethod->getName() === "__construct") {
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
142
					continue;
143
				}
144
145
				if ($definition->hasMethodCall($reflectionMethod->getName())) {
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
146
					continue;
147
				}
148
149
				if (strpos($reflectionMethod->getDocComment(), "@Autowired") === false) {
150
					continue;
151
				}
152
153
				/** @var Autowired $annotation */
154
				$annotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Autowired::class);
155
156
				if ($annotation === null) {
157
					continue;
158
				}
159
160
				if ($annotation->name !== null) {
161
					throw new AutowiringException(
162
						sprintf(
163
							"@Autowired parameter can be used only on properties. %s::%s(...)",
164
							$className,
165
							$reflectionMethod->getName()
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
166
						)
167
					);
168
				}
169
170
				$definition->addMethodCall(
171
					$reflectionMethod->getName(),
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
172
					$this->autowireMethod(
173
						$className,
174
						$reflectionMethod,
175
						[],
176 4
						$preferredServices
177
					)
178
				);
179 4
			}
180 4
181 1
			// properties @Autowired, @Value
182
			$manualProperties = $definition->getProperties();
183
			foreach ($reflectionClass->getProperties() as $reflectionProperty) {
184
				if (isset($manualProperties[$reflectionProperty->getName()])) {
185 1
					continue;
186
				}
187 1
188
				if (strpos($reflectionProperty->getDocComment(), "@Autowired") === false &&
189
					strpos($reflectionProperty->getDocComment(), "@Value") === false
190
				) {
191 1
					continue;
192
				}
193 1
194 1
				$annotations = $this->annotationReader->getPropertyAnnotations($reflectionProperty);
195 1
196
				$autowiredAnnotation = false;
197 1
				$valueAnnotation = false;
198 1
				$incorrectUsage = false;
199 1
200
				foreach ($annotations as $annotation) {
201
					if ($annotation instanceof Autowired) {
202
						if ($valueAnnotation) {
203
							$incorrectUsage = true;
204 1
							break;
205
						}
206
207 1
						$autowiredAnnotation = true;
208
209
						try {
210
							if ($annotation->name !== null) {
211
								$definition->setProperty(
212
									$reflectionProperty->getName(),
213 1
									new Reference($annotation->name)
214 1
								);
215 1
							} else {
216 1
								$definition->setProperty(
217 1
									$reflectionProperty->getName(),
218 1
									$this->getValue(
219 1
										$reflectionProperty,
220 1
										$reflectionProperty->getDocComment(),
221 1
										$preferredServices
222
									)
223 1
								);
224 1
							}
225
226
						} catch (AutowiringException $exception) {
227 1
							throw new AutowiringException(
228
								sprintf(
229
									"%s (Property %s::$%s)",
230
									$exception->getMessage(),
231
									$className,
232
									$reflectionProperty->getName()
233
								),
234
								$exception->getCode(),
235
								$exception
236
							);
237
						}
238
239
					} elseif ($annotation instanceof Value) {
240 1
						if ($autowiredAnnotation) {
241
							$incorrectUsage = true;
242
							break;
243
						}
244
245
						try {
246
							$definition->setProperty(
247
								$reflectionProperty->getName(),
248
								$parameterBag->resolveValue($annotation->value)
249
							);
250
						} catch (RuntimeException $exception) {
251
							throw new AutowiringException(
252
								sprintf(
253
									"%s (Property %s::$%s)",
254
									$exception->getMessage(),
255
									$className,
256
									$reflectionProperty->getName()
257
								),
258
								$exception->getCode(),
259
								$exception
260
							);
261
						}
262
					}
263
				}
264 1
265
				if ($incorrectUsage) {
266 1
					throw new AutowiringException(
267
						sprintf(
268
							"Property can have either @Autowired, or @Value annotation, not both. (Property %s::$%s)",
269
							$className,
270
							$reflectionProperty->getName()
271
						)
272
					);
273
				}
274
			}
275 4
276
		}
277 4
	}
278 4
279
	/**
280
	 * @param string $className
281
	 * @param ReflectionMethod $reflectionMethod
282
	 * @param array $arguments
283
	 * @param string[] $preferredServices
284
	 * @return array
285
	 */
286
	private function autowireMethod(
287 4
		string $className,
288
		ReflectionMethod $reflectionMethod,
289
		array $arguments,
290
		array $preferredServices
291
	): array {
292
		$outputArguments = [];
293 4
294
		foreach ($reflectionMethod->getParameters() as $i => $reflectionProperty) {
295 4
			// intentionally array_key_exists() instead of isset(), isset() would return false if argument is null
296
			if (array_key_exists($i, $arguments)) {
297 4
				$outputArguments[$i] = $arguments[$i];
298
			} else if (array_key_exists($reflectionProperty->getName(), $arguments)) {
0 ignored issues
show
Bug introduced by
Consider using $reflectionProperty->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
299 4
				$outputArguments[$i] = $arguments[$reflectionProperty->getName()];
0 ignored issues
show
Bug introduced by
Consider using $reflectionProperty->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
300 1
			} else {
301 1
				try {
302
					$outputArguments[$i] = $this->getValue(
303 4
						$reflectionProperty,
304 4
						$reflectionMethod->getDocComment(),
305 4
						$preferredServices
306 4
					);
307 4
308 4
				} catch (AutowiringException $exception) {
309 4
					throw new AutowiringException(
310
						sprintf(
311 4
							"%s (%s::%s(%s$%s%s))",
312
							$exception->getMessage(),
313 4
							$className,
314 1
							$reflectionMethod->getName(),
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
315 1
							$reflectionProperty->getPosition() !== 0 ? "..., " : "",
316 1
							$reflectionProperty->getName(),
0 ignored issues
show
Bug introduced by
Consider using $reflectionProperty->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
317 1
							$reflectionProperty->getPosition() < $reflectionMethod->getNumberOfParameters() - 1
318 1
								? ", ..."
319 1
								: ""
320 1
						),
321 1
						$exception->getCode(),
322 1
						$exception
323 1
					);
324
				}
325 1
			}
326 1
		}
327
328 1
		return $outputArguments;
329
	}
330
331 3
	/**
332
	 * @param ReflectionProperty|ReflectionParameter $target
333 3
	 * @param string $docComment
334
	 * @param $preferredServices
335
	 * @return mixed
336
	 */
337
	private function getValue($target, $docComment, $preferredServices)
338
	{
339
		$className = null;
340
		$isArray = false;
341
342
		// resolve class name, whether value is array
343
		if ($target instanceof ReflectionParameter) { // parse parameter class
344
			if ($target->getClass() !== null) {
345
				$className = $target->getClass()->getName();
0 ignored issues
show
Bug introduced by
Consider using $target->getClass()->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
346 5
347
			} elseif (preg_match(
348
				"/@param\\s+([a-zA-Z0-9\\\\_]+)(\\[\\])?(\\|[^\\s]+)*\\s+\\\$" . preg_quote($target->getName()) . "/",
0 ignored issues
show
Bug introduced by
Consider using $target->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
349
				$docComment,
350
				$m
351
			)) {
352
				$className = $m[1];
353
				$isArray = isset($m[2]) && $m[2] === "[]";
354
355 5
			} elseif (!$target->isDefaultValueAvailable()) {
356 5
				throw new AutowiringException(sprintf(
357
					"Could not parse parameter type of class %s - neither type hint, nor @param annotation available.",
358
					$target->getDeclaringClass()->getName()
0 ignored issues
show
Bug introduced by
Consider using $target->getDeclaringClass()->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
359 5
				));
360 4
			}
361 4
362
		} else if ($target instanceof ReflectionProperty) { // parse property class
363 4
			if (preg_match("/@var\\s+([a-zA-Z0-9\\\\_]+)(\\[\\])?/", $docComment, $m)) {
364
				$className = $m[1];
365
				$isArray = isset($m[2]) && $m[2] === "[]";
366
367
			} else {
368
				throw new AutowiringException(
369
					"Could not parse property type - no @var annotation."
370
				);
371
			}
372
		}
373
374
		// resolve class name to FQN
375
		$lowerClassName = trim(strtolower($className), "\\ \t\n");
376
		$useStatements = $this->getUseStatements($target);
377 4
		if (isset($useStatements[$lowerClassName])) {
378 1
			$className = $useStatements[$lowerClassName];
379 1
		} elseif (strpos($className, "\\") === false) {
380 1
			$className = $target->getDeclaringClass()->getNamespaceName() . "\\" . $className;
381
		}
382 1
383
		$className = trim($className, "\\");
384
385
		// autowire from class map
386
		if ($isArray) {
387
			return array_map(function ($serviceId) {
388
				return new Reference($serviceId);
389
			}, $this->classMap->getMulti($className));
390 5
391 5
		} elseif ($className !== null) {
392 5
			try {
393
				return new Reference($this->classMap->getSingle($className));
394 5
395 1
			} catch (NoValueException $exception) {
396 1
				if ($target instanceof ReflectionParameter && $target->isDefaultValueAvailable()) {
397
					return $target->getDefaultValue();
398 5
				} else {
399
					throw new AutowiringException(sprintf("Missing service of type '%s'.", $className));
400
				}
401 5
402
			} catch (MultipleValuesException $exception) {
403
				if (isset($preferredServices[$className])) {
404
					return new Reference($preferredServices[$className]);
405
				} else {
406 5
					throw new AutowiringException(sprintf("Multiple services of type '%s': %s", $className, $exception->getMessage()));
407
				}
408 5
			}
409
410 2
		} elseif ($target instanceof ReflectionParameter && $target->isDefaultValueAvailable()) {
411 1
			return $target->getDefaultValue();
412
413
		} else {
414 1
			throw new AutowiringException("Could not autowire.");
415
		}
416
	}
417 1
418 1
	/**
419 1
	 * @param ReflectionProperty|ReflectionParameter $target
420
	 * @return string[]
421
	 */
422
	private function getUseStatements($target): array
423
	{
424
		$class = $target->getDeclaringClass();
425
		$useStatements = $this->getClassUseStatements($class);
426
		foreach ($class->getTraits() as $trait) {
427
			if ($target instanceof ReflectionParameter &&
428
				$trait->hasMethod($target->getDeclaringFunction()->getName()) &&
0 ignored issues
show
Bug introduced by
Consider using $target->getDeclaringFunction()->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
429
				$target->getDeclaringFunction()->getFileName() === $trait->getFileName()
430
			) {
431
				$useStatements = array_merge($useStatements, $this->getClassUseStatements($trait));
432
			} else if ($target instanceof ReflectionProperty &&
433
				$trait->hasProperty($target->getName())
434
			) {
435
				$useStatements = array_merge($useStatements, $this->getClassUseStatements($trait));
436
			}
437 5
		}
438
		return $useStatements;
439 5
	}
440 5
441 5
	private function getClassUseStatements(ReflectionClass $reflectionClass): array
442
	{
443 5
		if (!isset($this->cachedUseStatements[$reflectionClass->getName()])) {
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
444
			$this->cachedUseStatements[$reflectionClass->getName()] = $this->phpParser->parseClass($reflectionClass);
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
445
		}
446
447
		return $this->cachedUseStatements[$reflectionClass->getName()];
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
448
	}
449 5
450
	/**
451
	 * @return array
452 5
	 */
453 5
	private function getIgnoredServicePatterns(): array
454 5
	{
455
		try {
456
			return (array)$this->parameterBag->resolveValue("%autowiring.ignored_services%");
457
		} catch (ParameterNotFoundException $exception) {
458
			return [];
459
		}
460
	}
461
462
	/**
463 5
	 * @param string $serviceId
464
	 * @param Definition $definition
465 5
	 * @return bool
466
	 */
467
	private function canDefinitionBeAutowired($serviceId, Definition $definition): bool
468
	{
469
		if (preg_match('/^\d+_[^~]++~[._a-zA-Z\d]{7}$/', $serviceId)) {
470
			return false;
471 5
		}
472
473 5
		foreach ($this->getIgnoredServicePatterns() as $pattern) {
474 5
			if (($pattern[0] === "/" && preg_match($pattern, $serviceId)) ||
475 5
				strcasecmp($serviceId, $pattern) == 0
476 5
			) {
477 5
				return false;
478 5
			}
479
		}
480
481
		if ($definition->isAbstract() ||
482 5
			$definition->isSynthetic() ||
483
			!$definition->getClass() ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $definition->getClass() of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
484
			$definition->getFactory()
485
		) {
486
			return false;
487
		}
488
489
		return true;
490
	}
491
492
}
493