Completed
Push — master ( 90cc4f...febf96 )
by
unknown
9s
created

AutowiringCompilerPass   C

Complexity

Total Complexity 72

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 63.64%

Importance

Changes 0
Metric Value
wmc 72
lcom 1
cbo 11
dl 0
loc 464
c 0
b 0
f 0
ccs 147
cts 231
cp 0.6364
rs 5.5667

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
C process() 0 51 7
D autowireClass() 0 167 24
C autowireMethod() 0 48 8
D getValue() 0 87 18
A getUseStatements() 0 8 2
A getIgnoredServicePatterns() 0 8 2
C canDefinitionBeAutowired() 0 24 10

How to fix   Complexity   

Complex Class

Complex classes like AutowiringCompilerPass often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AutowiringCompilerPass, and based on these observations, apply Extract Interface, too.

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