AutowiringCompilerPass::autowireClass()   D
last analyzed

Complexity

Conditions 24
Paths 172

Size

Total Lines 167
Code Lines 104

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 128.9957

Importance

Changes 0
Metric Value
dl 0
loc 167
c 0
b 0
f 0
ccs 42
cts 97
cp 0.433
rs 4.345
cc 24
eloc 104
nc 172
nop 6
crap 128.9957

How to fix   Long Method    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($reflectionMethod, Autowired::class);
152
153
				if ($annotation === null) {
154
					continue;
155
				}
156
157
				if ($annotation->name !== null) {
158
					throw new AutowiringException(
159
						sprintf(
160
							"@Autowired parameter can be used only on properties. %s::%s(...)",
161
							$className,
162
							$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...
163
						)
164
					);
165
				}
166
167
				$definition->addMethodCall(
168
					$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...
169
					$this->autowireMethod(
170
						$className,
171
						$reflectionMethod,
172
						[],
173
						$preferredServices
174
					)
175
				);
176
			}
177
178
			// properties @Autowired, @Value
179 4
			$manualProperties = $definition->getProperties();
180 4
			foreach ($reflectionClass->getProperties() as $reflectionProperty) {
181 1
				if (isset($manualProperties[$reflectionProperty->getName()])) {
182
					continue;
183
				}
184
185 1
				if (strpos($reflectionProperty->getDocComment(), "@Autowired") === false &&
186 1
					strpos($reflectionProperty->getDocComment(), "@Value") === false
187
				) {
188
					continue;
189
				}
190
191 1
				$annotations = $this->annotationReader->getPropertyAnnotations($reflectionProperty);
192
193 1
				$autowiredAnnotation = false;
194 1
				$valueAnnotation = false;
195 1
				$incorrectUsage = false;
196
197 1
				foreach ($annotations as $annotation) {
198 1
					if ($annotation instanceof Autowired) {
199 1
						if ($valueAnnotation) {
200
							$incorrectUsage = true;
201
							break;
202
						}
203
204 1
						$autowiredAnnotation = true;
205
206
						try {
207 1
							if ($annotation->name !== null) {
208
								$definition->setProperty(
209
									$reflectionProperty->getName(),
210
									new Reference($annotation->name)
211
								);
212
							} else {
213 1
								$definition->setProperty(
214 1
									$reflectionProperty->getName(),
215 1
									$this->getValue(
216 1
										$reflectionProperty->getDeclaringClass(),
217 1
										$reflectionProperty->getDocComment(),
218 1
										null,
219 1
										null,
220 1
										false,
221 1
										null,
222 1
										$preferredServices
223
									)
224
								);
225
							}
226
227
						} catch (AutowiringException $exception) {
228
							throw new AutowiringException(
229
								sprintf(
230
									"%s (Property %s::$%s)",
231
									$exception->getMessage(),
232
									$className,
233
									$reflectionProperty->getName()
234
								),
235
								$exception->getCode(),
236 1
								$exception
237
							);
238
						}
239
240
					} elseif ($annotation instanceof Value) {
241
						if ($autowiredAnnotation) {
242
							$incorrectUsage = true;
243
							break;
244
						}
245
246
						try {
247
							$definition->setProperty(
248
								$reflectionProperty->getName(),
249
								$parameterBag->resolveValue($annotation->value)
250
							);
251
						} catch (RuntimeException $exception) {
252
							throw new AutowiringException(
253
								sprintf(
254
									"%s (Property %s::$%s)",
255
									$exception->getMessage(),
256
									$className,
257
									$reflectionProperty->getName()
258
								),
259
								$exception->getCode(),
260 1
								$exception
261
							);
262
						}
263
					}
264
				}
265
266 1
				if ($incorrectUsage) {
267
					throw new AutowiringException(
268
						sprintf(
269
							"Property can have either @Autowired, or @Value annotation, not both. (Property %s::$%s)",
270
							$className,
271 1
							$reflectionProperty->getName()
272
						)
273
					);
274
				}
275
			}
276
277
		}
278 4
	}
279
280
	/**
281
	 * @param string $className
282
	 * @param ReflectionMethod $reflectionMethod
283
	 * @param array $arguments
284
	 * @param string[] $preferredServices
285
	 * @return array
286
	 */
287 4
	private function autowireMethod(
288
		string $className,
289
		ReflectionMethod $reflectionMethod,
290
		array $arguments,
291
		array $preferredServices
292
	): array {
293 4
		$outputArguments = [];
294
295 4
		foreach ($reflectionMethod->getParameters() as $i => $reflectionProperty) {
296
			// 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...
297 4
			if (array_key_exists($i, $arguments)) {
298
				$outputArguments[$i] = $arguments[$i];
299 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...
300 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...
301
			} else {
302
				try {
303 4
					$outputArguments[$i] = $this->getValue(
304 4
						$reflectionProperty->getDeclaringClass(),
305 4
						$reflectionMethod->getDocComment(),
306 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...
307 4
						$reflectionProperty->getClass(),
308 4
						$reflectionProperty->isDefaultValueAvailable(),
309 4
						$reflectionProperty->isDefaultValueAvailable() ? $reflectionProperty->getDefaultValue() : null,
310 4
						$preferredServices
311
					);
312
313 1
				} catch (AutowiringException $exception) {
314 1
					throw new AutowiringException(
315 1
						sprintf(
316 1
							"%s (%s::%s(%s$%s%s))",
317 1
							$exception->getMessage(),
318 1
							$className,
319 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...
320 1
							$reflectionProperty->getPosition() !== 0 ? "..., " : "",
321 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...
322 1
							$reflectionProperty->getPosition() < $reflectionMethod->getNumberOfParameters() - 1
323
								? ", ..."
324 1
								: ""
325
						),
326 1
						$exception->getCode(),
327 4
						$exception
328
					);
329
				}
330
			}
331
		}
332
333 3
		return $outputArguments;
334
	}
335
336
	/**
337
	 * @param ReflectionClass $reflectionClass
338
	 * @param string $docComment
339
	 * @param string $parameterName
340
	 * @param ReflectionClass $parameterReflectionClass
341
	 * @param mixed $defaultValueAvailable
342
	 * @param mixed $defaultValue
343
	 * @param $preferredServices
344
	 * @return mixed
345
	 */
346 5
	private function getValue(
347
		ReflectionClass $reflectionClass,
348
		$docComment,
349
		$parameterName,
350
		ReflectionClass $parameterReflectionClass = null,
351
		$defaultValueAvailable,
352
		$defaultValue,
353
		$preferredServices
354
	) {
355 5
		$className = null;
356 5
		$isArray = false;
357
358
		// resolve class name, whether value is array
359 5
		if ($parameterName !== null) { // parse parameter class
360 4
			if ($parameterReflectionClass) {
361 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...
362
363
			} elseif (preg_match(
364
				"/@param\\s+([a-zA-Z0-9\\\\_]+)(\\[\\])?(\\|[^\\s]+)*\\s+\\\$" . preg_quote($parameterName) . "/",
365
				$docComment,
366
				$m
367
			)) {
368
				$className = $m[1];
369
				$isArray = isset($m[2]) && $m[2] === "[]";
370
371
			} elseif (!$defaultValueAvailable) {
372
				throw new AutowiringException(sprintf(
373
					"Could not parse parameter type of class %s - neither type hint, nor @param annotation available.",
374 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...
375
				));
376
			}
377
378
		} else { // parse property class
379 1
			if (preg_match("/@var\\s+([a-zA-Z0-9\\\\_]+)(\\[\\])?/", $docComment, $m)) {
380 1
				$className = $m[1];
381 1
				$isArray = isset($m[2]) && $m[2] === "[]";
382
383
			} elseif (!$defaultValueAvailable) {
384
				throw new AutowiringException(
385
					"Could not parse property type - no @var annotation."
386
				);
387
			}
388
		}
389
390
		// resolve class name to FQN
391 5
		$lowerClassName = trim(strtolower($className), "\\ \t\n");
392 5
		$useStatements = $this->getUseStatements($reflectionClass);
393 5
		if (isset($useStatements[$lowerClassName])) {
394
			$className = $useStatements[$lowerClassName];
395 5
		} elseif (strpos($className, "\\") === false) {
396 1
			$className = $reflectionClass->getNamespaceName() . "\\" . $className;
397
		}
398
399 5
		$className = trim($className, "\\");
400
401
		// autowire from class map
402 5
		if ($isArray) {
403
			return array_map(function ($serviceId) {
404
				return new Reference($serviceId);
405
			}, $this->classMap->getMulti($className));
406
407 5
		} elseif ($className !== null) {
408
			try {
409 5
				return new Reference($this->classMap->getSingle($className));
410
411 2
			} catch (NoValueException $exception) {
412 1
				if ($defaultValueAvailable) {
413
					return $defaultValue;
414
				} else {
415 1
					throw new AutowiringException(sprintf("Missing service of type '%s'.", $className));
416
				}
417
418 1
			} catch (MultipleValuesException $exception) {
419 1
				if (isset($preferredServices[$className])) {
420 1
					return new Reference($preferredServices[$className]);
421
				} else {
422
					throw new AutowiringException(sprintf("Multiple services of type '%s': %s", $className, $exception->getMessage()));
423
				}
424
			}
425
426
		} elseif ($defaultValueAvailable) {
427
			return $defaultValue;
428
429
		} else {
430
			throw new AutowiringException("Could not autowire.");
431
		}
432
	}
433
434
	/**
435
	 * @param ReflectionClass $reflectionClass
436
	 * @return string[]
437
	 */
438 5
	public function getUseStatements(ReflectionClass $reflectionClass): array
439
	{
440 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...
441 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...
442
		}
443
444 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...
445
	}
446
447
	/**
448
	 * @return array
449
	 */
450 5
	private function getIgnoredServicePatterns(): array
451
	{
452
		try {
453 5
			return (array)$this->parameterBag->resolveValue("%autowiring.ignored_services%");
454 5
		} catch (ParameterNotFoundException $exception) {
455 5
			return [];
456
		}
457
	}
458
459
	/**
460
	 * @param string $serviceId
461
	 * @param Definition $definition
462
	 * @return bool
463
	 */
464 5
	private function canDefinitionBeAutowired($serviceId, Definition $definition): bool
465
	{
466 5
		if (preg_match('/^\d+_[^~]++~[._a-zA-Z\d]{7}$/', $serviceId)) {
467
			return false;
468
		}
469
470 5
		foreach ($this->getIgnoredServicePatterns() as $pattern) {
471
			if (($pattern[0] === "/" && preg_match($pattern, $serviceId)) ||
472
				strcasecmp($serviceId, $pattern) == 0
473
			) {
474
				return false;
475
			}
476
		}
477
478 5
		if ($definition->isAbstract() ||
479 5
			$definition->isSynthetic() ||
480 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...
481 5
			$definition->getFactory()
482
		) {
483 5
			return false;
484
		}
485
486 5
		return true;
487
	}
488
489
}
490