Completed
Pull Request — master (#10)
by Jakub
03:36
created

AutowiringCompilerPass::canDefinitionBeAutowired()   C

Complexity

Conditions 10
Paths 6

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 12.9132

Importance

Changes 0
Metric Value
dl 0
loc 24
c 0
b 0
f 0
ccs 9
cts 13
cp 0.6923
rs 5.2164
cc 10
eloc 13
nc 6
nop 2
crap 12.9132

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 RuntimeException;
10
use Skrz\Bundle\AutowiringBundle\Annotation\Autowired;
11
use Skrz\Bundle\AutowiringBundle\Annotation\Value;
12
use Skrz\Bundle\AutowiringBundle\DependencyInjection\ClassMultiMap;
13
use Skrz\Bundle\AutowiringBundle\Exception\AutowiringException;
14
use Skrz\Bundle\AutowiringBundle\Exception\MultipleValuesException;
15
use Skrz\Bundle\AutowiringBundle\Exception\NoValueException;
16
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17
use Symfony\Component\DependencyInjection\ContainerBuilder;
18
use Symfony\Component\DependencyInjection\Definition;
19
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
20
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
21
use Symfony\Component\DependencyInjection\Reference;
22
23
/**
24
 * @author Jakub Kulhan <[email protected]>
25
 */
26
class AutowiringCompilerPass implements CompilerPassInterface
27
{
28
29
	/** @var ClassMultiMap */
30
	private $classMap;
31
32
	/** @var AnnotationReader */
33
	private $annotationReader;
34
35
	/** @var PhpParser */
36
	private $phpParser;
37
38
	/** @var string[][] */
39
	private $cachedUseStatements = [];
40
41
	/** @var ParameterBagInterface */
42
	private $parameterBag;
43
44 6
	public function __construct(ClassMultiMap $classMap, AnnotationReader $annotationReader, PhpParser $phpParser)
45
	{
46 6
		$this->classMap = $classMap;
47 6
		$this->annotationReader = $annotationReader;
48 6
		$this->phpParser = $phpParser;
49 6
		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...
50 6
	}
51
52 5
	public function process(ContainerBuilder $container)
53
	{
54 5
		$this->parameterBag = $parameterBag = $container->getParameterBag();
55
56
		try {
57 5
			$preferredServices = (array)$parameterBag->resolveValue("%autowiring.preferred_services%");
58 4
		} catch (ParameterNotFoundException $exception) {
59 4
			$preferredServices = [];
60
		}
61
62
		try {
63 5
			$fastAnnotationChecksRegex = "/" . implode("|", array_map(function ($s) {
64
					return preg_quote($s);
65 5
				}, (array)$parameterBag->resolveValue("%autowiring.fast_annotation_checks%"))) . "/";
66 5
		} catch (ParameterNotFoundException $exception) {
67 5
			$fastAnnotationChecksRegex = null;
68
		}
69
70 5
		foreach ($container->getDefinitions() as $serviceId => $definition) {
71 5
			if ($this->canDefinitionBeAutowired($serviceId, $definition) === false) {
72 5
				continue;
73
			}
74
75
			try {
76 5
				$className = $parameterBag->resolveValue($definition->getClass());
77 5
				$reflectionClass = $container->getReflectionClass($className, false);
78 5
				if ($reflectionClass === null) {
79
					continue;
80
				}
81
82 5
				$this->autowireClass(
83 5
					$className,
84 5
					$reflectionClass,
85 5
					$definition,
86 5
					$fastAnnotationChecksRegex,
87 5
					$preferredServices,
88 5
					$parameterBag
89
				);
90
91
				// add files to cache
92 4
				$container->addObjectResource($reflectionClass);
93
94 1
			} catch (AutowiringException $exception) {
95 1
				throw new AutowiringException(
96 1
					sprintf("%s (service: %s)", $exception->getMessage(), $serviceId),
97 1
					$exception->getCode(),
98 5
					$exception
99
				);
100
			}
101
		}
102 4
	}
103
104
	/**
105
	 * @param string $className
106
	 * @param ReflectionClass $reflectionClass
107
	 * @param Definition $definition
108
	 * @param string $fastAnnotationChecksRegex
109
	 * @param string[] $preferredServices
110
	 * @param ParameterBagInterface $parameterBag
111
	 */
112 5
	private function autowireClass(
113
		string $className,
114
		ReflectionClass $reflectionClass,
115
		Definition $definition,
116
		?string $fastAnnotationChecksRegex,
117
		array $preferredServices,
118
		ParameterBagInterface $parameterBag
119
	) {
120
		// constructor - autowire always
121 5
		if ($reflectionClass->getConstructor()) {
122 4
			$definition->setArguments(
123 4
				$this->autowireMethod(
124 4
					$className,
125 4
					$reflectionClass->getConstructor(),
126 4
					$definition->getArguments(),
127 4
					$preferredServices
128
				)
129
			);
130
		}
131
132 4
		if ($fastAnnotationChecksRegex === null ||
133
			($reflectionClass->getDocComment() &&
134 4
				preg_match($fastAnnotationChecksRegex, $reflectionClass->getDocComment()))
135
		) {
136
			// method calls @Autowired
137 4
			foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
138 3
				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...
139 3
					continue;
140
				}
141
142
				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...
143
					continue;
144
				}
145
146
				if (strpos($reflectionMethod->getDocComment(), "@Autowired") === false) {
147
					continue;
148
				}
149
150
				/** @var Autowired $annotation */
151
				$annotation = $this->annotationReader->getMethodAnnotation(
152
					$reflectionMethod,
153
					"Skrz\\Bundle\\AutowiringBundle\\Annotation\\Autowired"
154
				);
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
						$preferredServices
177
					)
178
				);
179
			}
180
181
			// properties @Autowired, @Value
182 4
			$manualProperties = $definition->getProperties();
183 4
			foreach ($reflectionClass->getProperties() as $reflectionProperty) {
184 1
				if (isset($manualProperties[$reflectionProperty->getName()])) {
185
					continue;
186
				}
187
188 1
				if (strpos($reflectionProperty->getDocComment(), "@Autowired") === false &&
189 1
					strpos($reflectionProperty->getDocComment(), "@Value") === false
190
				) {
191
					continue;
192
				}
193
194 1
				$annotations = $this->annotationReader->getPropertyAnnotations($reflectionProperty);
195
196 1
				$autowiredAnnotation = false;
197 1
				$valueAnnotation = false;
198 1
				$incorrectUsage = false;
199
200 1
				foreach ($annotations as $annotation) {
201 1
					if ($annotation instanceof Autowired) {
202 1
						if ($valueAnnotation) {
203
							$incorrectUsage = true;
204
							break;
205
						}
206
207 1
						$autowiredAnnotation = true;
208
209
						try {
210 1
							if ($annotation->name !== null) {
211
								$definition->setProperty(
212
									$reflectionProperty->getName(),
213
									new Reference($annotation->name)
214
								);
215
							} else {
216 1
								$definition->setProperty(
217 1
									$reflectionProperty->getName(),
218 1
									$this->getValue(
219 1
										$reflectionProperty->getDeclaringClass(),
220 1
										$reflectionProperty->getDocComment(),
221 1
										null,
222 1
										null,
223 1
										false,
224 1
										null,
225 1
										$preferredServices
226
									)
227
								);
228
							}
229
230
						} catch (AutowiringException $exception) {
231
							throw new AutowiringException(
232
								sprintf(
233
									"%s (Property %s::$%s)",
234
									$exception->getMessage(),
235
									$className,
236
									$reflectionProperty->getName()
237
								),
238
								$exception->getCode(),
239 1
								$exception
240
							);
241
						}
242
243
					} elseif ($annotation instanceof Value) {
244
						if ($autowiredAnnotation) {
245
							$incorrectUsage = true;
246
							break;
247
						}
248
249
						try {
250
							$definition->setProperty(
251
								$reflectionProperty->getName(),
252
								$parameterBag->resolveValue($annotation->value)
253
							);
254
						} catch (RuntimeException $exception) {
255
							throw new AutowiringException(
256
								sprintf(
257
									"%s (Property %s::$%s)",
258
									$exception->getMessage(),
259
									$className,
260
									$reflectionProperty->getName()
261
								),
262
								$exception->getCode(),
263 1
								$exception
264
							);
265
						}
266
					}
267
				}
268
269 1
				if ($incorrectUsage) {
270
					throw new AutowiringException(
271
						sprintf(
272
							"Property can have either @Autowired, or @Value annotation, not both. (Property %s::$%s)",
273
							$className,
274 1
							$reflectionProperty->getName()
275
						)
276
					);
277
				}
278
			}
279
280
		}
281 4
	}
282
283
	/**
284
	 * @param string $className
285
	 * @param ReflectionMethod $reflectionMethod
286
	 * @param array $arguments
287
	 * @param string[] $preferredServices
288
	 * @return array
289
	 */
290 4
	private function autowireMethod(
291
		string $className,
292
		ReflectionMethod $reflectionMethod,
293
		array $arguments,
294
		array $preferredServices
295
	): array {
296 4
		$outputArguments = [];
297
298 4
		foreach ($reflectionMethod->getParameters() as $i => $reflectionProperty) {
299
			// intentionally array_key_exists() instead of isset(), isset() would return false if argument is null
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
300 4
			if (array_key_exists($i, $arguments)) {
301
				$outputArguments[$i] = $arguments[$i];
302 4
			} 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...
303 1
				$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...
304
			} else {
305
				try {
306 4
					$outputArguments[$i] = $this->getValue(
307 4
						$reflectionProperty->getDeclaringClass(),
308 4
						$reflectionMethod->getDocComment(),
309 4
						$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...
310 4
						$reflectionProperty->getClass(),
311 4
						$reflectionProperty->isDefaultValueAvailable(),
312 4
						$reflectionProperty->isDefaultValueAvailable() ? $reflectionProperty->getDefaultValue() : null,
313 4
						$preferredServices
314
					);
315
316 1
				} catch (AutowiringException $exception) {
317 1
					throw new AutowiringException(
318 1
						sprintf(
319 1
							"%s (%s::%s(%s$%s%s))",
320 1
							$exception->getMessage(),
321 1
							$className,
322 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...
323 1
							$reflectionProperty->getPosition() !== 0 ? "..., " : "",
324 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...
325 1
							$reflectionProperty->getPosition() < $reflectionMethod->getNumberOfParameters() - 1
326
								? ", ..."
327 1
								: ""
328
						),
329 1
						$exception->getCode(),
330 4
						$exception
331
					);
332
				}
333
			}
334
		}
335
336 3
		return $outputArguments;
337
	}
338
339
	/**
340
	 * @param ReflectionClass $reflectionClass
341
	 * @param string $docComment
342
	 * @param string $parameterName
343
	 * @param ReflectionClass $parameterReflectionClass
344
	 * @param mixed $defaultValueAvailable
345
	 * @param mixed $defaultValue
346
	 * @param $preferredServices
347
	 * @return mixed
348
	 */
349 5
	private function getValue(
350
		ReflectionClass $reflectionClass,
351
		$docComment,
352
		$parameterName,
353
		ReflectionClass $parameterReflectionClass = null,
354
		$defaultValueAvailable,
355
		$defaultValue,
356
		$preferredServices
357
	) {
358 5
		$className = null;
359 5
		$isArray = false;
360
361
		// resolve class name, whether value is array
362 5
		if ($parameterName !== null) { // parse parameter class
363 4
			if ($parameterReflectionClass) {
364 4
				$className = $parameterReflectionClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $parameterReflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
365
366
			} elseif (preg_match(
367
				"/@param\\s+([a-zA-Z0-9\\\\_]+)(\\[\\])?(\\|[^\\s]+)*\\s+\\\$" . preg_quote($parameterName) . "/",
368
				$docComment,
369
				$m
370
			)) {
371
				$className = $m[1];
372
				$isArray = isset($m[2]) && $m[2] === "[]";
373
374
			} elseif (!$defaultValueAvailable) {
375
				throw new AutowiringException(sprintf(
376
					"Could not parse parameter type of class %s - neither type hint, nor @param annotation available.",
377 4
					$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...
378
				));
379
			}
380
381
		} else { // parse property class
382 1
			if (preg_match("/@var\\s+([a-zA-Z0-9\\\\_]+)(\\[\\])?/", $docComment, $m)) {
383 1
				$className = $m[1];
384 1
				$isArray = isset($m[2]) && $m[2] === "[]";
385
386
			} elseif (!$defaultValueAvailable) {
387
				throw new AutowiringException(
388
					"Could not parse property type - no @var annotation."
389
				);
390
			}
391
		}
392
393
		// resolve class name to FQN
394 5
		$lowerClassName = trim(strtolower($className), "\\ \t\n");
395 5
		$useStatements = $this->getUseStatements($reflectionClass);
396 5
		if (isset($useStatements[$lowerClassName])) {
397
			$className = $useStatements[$lowerClassName];
398 5
		} elseif (strpos($className, "\\") === false) {
399 1
			$className = $reflectionClass->getNamespaceName() . "\\" . $className;
400
		}
401
402 5
		$className = trim($className, "\\");
403
404
		// autowire from class map
405 5
		if ($isArray) {
406
			return array_map(function ($serviceId) {
407
				return new Reference($serviceId);
408
			}, $this->classMap->getMulti($className));
409
410 5
		} elseif ($className !== null) {
411
			try {
412 5
				return new Reference($this->classMap->getSingle($className));
413
414 2
			} catch (NoValueException $exception) {
415 1
				if ($defaultValueAvailable) {
416
					return $defaultValue;
417
				} else {
418 1
					throw new AutowiringException(sprintf("Missing service of type '%s'.", $className));
419
				}
420
421 1
			} catch (MultipleValuesException $exception) {
422 1
				if (isset($preferredServices[$className])) {
423 1
					return new Reference($preferredServices[$className]);
424
				} else {
425
					throw new AutowiringException(sprintf("Multiple services of type '%s': %s", $className, $exception->getMessage()));
426
				}
427
			}
428
429
		} elseif ($defaultValueAvailable) {
430
			return $defaultValue;
431
432
		} else {
433
			throw new AutowiringException("Could not autowire.");
434
		}
435
	}
436
437
	/**
438
	 * @param ReflectionClass $reflectionClass
439
	 * @return string[]
440
	 */
441 5
	public function getUseStatements(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 5
			$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 5
		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
450
	/**
451
	 * @return array
452
	 */
453 5
	private function getIgnoredServicePatterns(): array
454
	{
455
		try {
456 5
			return (array)$this->parameterBag->resolveValue("%autowiring.ignored_services%");
457 5
		} catch (ParameterNotFoundException $exception) {
458 5
			return [];
459
		}
460
	}
461
462
	/**
463
	 * @param string $serviceId
464
	 * @param Definition $definition
465
	 * @return bool
466
	 */
467 5
	private function canDefinitionBeAutowired($serviceId, Definition $definition): bool
468
	{
469 5
		if (preg_match('/^\d+_[^~]++~[._a-zA-Z\d]{7}$/', $serviceId)) {
470
			return false;
471
		}
472
473 5
		foreach ($this->getIgnoredServicePatterns() as $pattern) {
474
			if (($pattern[0] === "/" && preg_match($pattern, $serviceId)) ||
475
				strcasecmp($serviceId, $pattern) == 0
476
			) {
477
				return false;
478
			}
479
		}
480
481 5
		if ($definition->isAbstract() ||
482 5
			$definition->isSynthetic() ||
483 5
			!$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 5
			$definition->getFactory()
485
		) {
486 5
			return false;
487
		}
488
489 5
		return true;
490
	}
491
492
}
493