Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like ContainerBuilder 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 ContainerBuilder, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types = 1); |
||
21 | class ContainerBuilder |
||
22 | { |
||
23 | /** Controller classes scope name */ |
||
24 | const SCOPE_CONTROLLER = 'controllers'; |
||
25 | |||
26 | /** Service classes scope name */ |
||
27 | const SCOPE_SERVICES = 'services'; |
||
28 | |||
29 | /** Generated resolving function name prefix */ |
||
30 | const DI_FUNCTION_PREFIX = 'container'; |
||
31 | |||
32 | /** Generated resolving function service static collection name */ |
||
33 | const DI_FUNCTION_SERVICES = '$' . self::SCOPE_SERVICES; |
||
34 | |||
35 | /** @var string[] Collection of available container scopes */ |
||
36 | protected $scopes = [ |
||
37 | self::SCOPE_CONTROLLER => [], |
||
38 | self::SCOPE_SERVICES => [] |
||
39 | ]; |
||
40 | |||
41 | /** @var ClassMetadata[] Collection of classes metadata */ |
||
42 | protected $classMetadata = []; |
||
43 | |||
44 | /** @var array Collection of dependencies aliases */ |
||
45 | protected $classAliases = []; |
||
46 | |||
47 | /** |
||
48 | * @var FileManagerInterface |
||
49 | * @Injectable |
||
50 | */ |
||
51 | protected $fileManger; |
||
52 | |||
53 | /** |
||
54 | * @var ResolverInterface |
||
55 | * @Injectable |
||
56 | */ |
||
57 | protected $classResolver; |
||
58 | |||
59 | /** |
||
60 | * @var Generator |
||
61 | * @Injectable |
||
62 | */ |
||
63 | protected $generator; |
||
64 | |||
65 | /** @var string Resolver function name */ |
||
66 | protected $resolverFunction; |
||
67 | |||
68 | /** |
||
69 | * Container constructor. |
||
70 | * |
||
71 | * @param FileManagerInterface $fileManager |
||
72 | * @param ResolverInterface $classResolver |
||
73 | * @param Generator $generator |
||
74 | */ |
||
75 | 11 | public function __construct( |
|
85 | |||
86 | /** |
||
87 | * Load classes from paths. |
||
88 | * |
||
89 | * @param array $paths Paths for importing |
||
90 | * |
||
91 | * @return $this |
||
92 | */ |
||
93 | 1 | public function loadFromPaths(array $paths) |
|
104 | |||
105 | /** |
||
106 | * Load classes from class names collection. |
||
107 | * |
||
108 | * @param string[] $classes Collection of class names for resolving |
||
109 | * |
||
110 | * @return $this |
||
111 | */ |
||
112 | 10 | public function loadFromClassNames(array $classes) |
|
128 | |||
129 | /** |
||
130 | * Find class names defined in PHP code. |
||
131 | * |
||
132 | * @param string $php PHP code for scanning |
||
133 | * |
||
134 | * @return string[] Collection of found class names in php code |
||
135 | */ |
||
136 | 2 | protected function getDefinedClasses(string $php) : array |
|
164 | |||
165 | /** |
||
166 | * Load classes from PHP c ode. |
||
167 | * |
||
168 | * @param string $php PHP code |
||
169 | * |
||
170 | * @return $this |
||
171 | */ |
||
172 | 1 | public function loadFromCode($php) |
|
182 | |||
183 | /** |
||
184 | * Build container class. |
||
185 | * |
||
186 | * @param string|null $containerClass Container class name |
||
187 | * @param string $namespace Name space |
||
188 | * |
||
189 | * @return string Generated Container class code |
||
190 | * @throws \InvalidArgumentException |
||
191 | */ |
||
192 | 8 | public function build($containerClass = 'Container', $namespace = '') |
|
243 | |||
244 | /** |
||
245 | * Build dependency resolving function. |
||
246 | * |
||
247 | * @param string $functionName Function name |
||
248 | * |
||
249 | * @throws \InvalidArgumentException |
||
250 | */ |
||
251 | 8 | protected function buildDependencyResolver($functionName) |
|
265 | |||
266 | /** |
||
267 | * Generate logic conditions and their implementation for container and its delegates. |
||
268 | * |
||
269 | * @param string $inputVariable Input condition parameter variable name |
||
270 | * @param bool|false $started Flag if condition branching has been started |
||
271 | */ |
||
272 | 8 | public function generateConditions($inputVariable = '$alias', $started = false) |
|
273 | { |
||
274 | // Iterate all container dependencies |
||
275 | 8 | foreach ($this->classMetadata as $className => $classMetadata) { |
|
276 | // Generate condition statement to define if this class is needed |
||
277 | 8 | $conditionFunc = !$started ? 'defIfCondition' : 'defElseIfCondition'; |
|
278 | |||
279 | // Output condition branch |
||
280 | 8 | $this->generator->$conditionFunc( |
|
281 | 8 | $this->buildResolverCondition($inputVariable, $className, $classMetadata->name) |
|
282 | ); |
||
283 | |||
284 | // Define if this class has service scope |
||
285 | 8 | $isService = in_array($className, $this->scopes[self::SCOPE_SERVICES], true); |
|
286 | |||
287 | /** @var MethodMetadata[] Gather only valid method for container */ |
||
288 | 8 | $classValidMethods = $this->getValidClassMethodsMetadata($classMetadata->methodsMetadata); |
|
289 | |||
290 | /** @var PropertyMetadata[] Gather only valid property for container */ |
||
291 | 8 | $classValidProperties = $this->getValidClassPropertiesMetadata($classMetadata->propertiesMetadata); |
|
292 | |||
293 | // Define class or service variable |
||
294 | 8 | $staticContainerName = $isService |
|
295 | 8 | ? self::DI_FUNCTION_SERVICES . '[\'' . $classMetadata->name . '\']' |
|
296 | 8 | : '$temp'; |
|
297 | |||
298 | 8 | if ($isService) { |
|
299 | // Check if dependency was instantiated |
||
300 | 8 | $this->generator->defIfCondition('!array_key_exists(\'' . $className . '\', ' . self::DI_FUNCTION_SERVICES . ')'); |
|
301 | } |
||
302 | |||
303 | 8 | if (count($classValidMethods) || count($classValidProperties)) { |
|
304 | 8 | $this->generator->newLine($staticContainerName . ' = '); |
|
305 | 8 | $this->buildResolvingClassDeclaration($className); |
|
306 | 8 | $this->buildConstructorDependencies($classMetadata->methodsMetadata); |
|
307 | |||
308 | // Internal scope reflection variable |
||
309 | 8 | $reflectionVariable = '$reflectionClass'; |
|
310 | |||
311 | 8 | $this->buildReflectionClass($className, $classValidProperties, $classValidMethods, $reflectionVariable); |
|
312 | |||
313 | // Process class properties |
||
314 | 8 | foreach ($classValidProperties as $property) { |
|
315 | // If such property has the dependency |
||
316 | 8 | if ($property->dependency) { |
|
317 | // Set value via refection |
||
318 | 8 | $this->buildResolverPropertyDeclaration( |
|
319 | 8 | $property->name, |
|
320 | 8 | $property->dependency, |
|
321 | $staticContainerName, |
||
322 | $reflectionVariable, |
||
323 | 8 | $property->isPublic |
|
324 | ); |
||
325 | } |
||
326 | } |
||
327 | |||
328 | /** @var MethodMetadata $methodMetadata */ |
||
329 | 8 | foreach ($classValidMethods as $methodName => $methodMetadata) { |
|
330 | 8 | $this->buildResolverMethodDeclaration( |
|
331 | 8 | $methodMetadata->dependencies, |
|
332 | $methodName, |
||
333 | $staticContainerName, |
||
334 | $reflectionVariable, |
||
335 | 8 | $methodMetadata->isPublic |
|
336 | ); |
||
337 | } |
||
338 | |||
339 | 8 | if ($isService) { |
|
340 | 7 | $this->generator->endIfCondition(); |
|
341 | } |
||
342 | |||
343 | 8 | $this->generator->newLine()->newLine('return ' . $staticContainerName . ';'); |
|
344 | } else { |
||
345 | 8 | $this->generator->newLine('return '); |
|
346 | 8 | $this->buildResolvingClassDeclaration($className); |
|
347 | 8 | $this->buildConstructorDependencies($classMetadata->methodsMetadata); |
|
348 | |||
349 | 8 | if ($isService) { |
|
350 | 1 | $this->generator->endIfCondition()->newLine('return ' . $staticContainerName . ';'); |
|
351 | } |
||
352 | |||
353 | } |
||
354 | |||
355 | // Set flag that condition is started |
||
356 | 8 | $started = true; |
|
357 | } |
||
358 | 8 | } |
|
359 | |||
360 | /** |
||
361 | * Build resolving function condition. |
||
362 | * |
||
363 | * @param string $inputVariable Condition variable |
||
364 | * @param string $className |
||
365 | * @param string|null $alias |
||
366 | * |
||
367 | * @return string Condition code |
||
368 | */ |
||
369 | 8 | protected function buildResolverCondition(string $inputVariable, string $className, string $alias = null) : string |
|
380 | |||
381 | /** |
||
382 | * Get valid class methods metadata. |
||
383 | * |
||
384 | * @param MethodMetadata[] $classMethodsMetadata All class methods metadata |
||
385 | * |
||
386 | * @return array Valid class methods metadata |
||
387 | */ |
||
388 | 8 | protected function getValidClassMethodsMetadata(array $classMethodsMetadata) |
|
401 | |||
402 | /** |
||
403 | * Get valid class properties metadata. |
||
404 | * |
||
405 | * @param PropertyMetadata[] $classPropertiesMetadata All class properties metadata |
||
406 | * |
||
407 | * @return array Valid class properties metadata |
||
408 | */ |
||
409 | 8 | protected function getValidClassPropertiesMetadata(array $classPropertiesMetadata) |
|
422 | |||
423 | /** |
||
424 | * Build resolving function class block. |
||
425 | * |
||
426 | * @param string $className Class name for new instance creation |
||
427 | */ |
||
428 | 8 | protected function buildResolvingClassDeclaration(string $className) |
|
432 | |||
433 | /** |
||
434 | * Build constructor arguments injection. |
||
435 | * |
||
436 | * @param MethodMetadata[] $methodsMetaData |
||
437 | */ |
||
438 | 8 | protected function buildConstructorDependencies(array $methodsMetaData) |
|
467 | |||
468 | /** |
||
469 | * Build resolving function dependency argument. |
||
470 | * |
||
471 | * @param mixed $argument Dependency argument |
||
472 | */ |
||
473 | 8 | protected function buildResolverArgument($argument, $textFunction = 'newLine') |
|
474 | { |
||
475 | // This is a dependency which invokes resolving function |
||
476 | 8 | if (array_key_exists($argument, $this->classMetadata)) { |
|
477 | // Call container logic for this dependency |
||
478 | 8 | $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')'); |
|
479 | 1 | } elseif (array_key_exists($argument, $this->classAliases)) { |
|
480 | // Call container logic for this dependency |
||
481 | 1 | $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')'); |
|
482 | 1 | } elseif (is_string($argument)) { // String variable |
|
483 | 1 | $this->generator->$textFunction()->stringValue($argument); |
|
484 | } elseif (is_array($argument)) { // Dependency value is array |
||
485 | $this->generator->$textFunction()->arrayValue($argument); |
||
486 | } |
||
487 | 8 | } |
|
488 | |||
489 | /** |
||
490 | * Generate reflection class for private/protected methods or properties |
||
491 | * in current scope. |
||
492 | * |
||
493 | * @param string $className Reflection class source class name |
||
494 | * @param PropertyMetadata[] $propertiesMetadata Properties metadata |
||
495 | * @param MethodMetadata[] $methodsMetadata Methods metadata |
||
496 | * @param string $reflectionVariable Reflection class variable name |
||
497 | */ |
||
498 | 8 | protected function buildReflectionClass(string $className, array $propertiesMetadata, array $methodsMetadata, string $reflectionVariable) |
|
537 | |||
538 | /** |
||
539 | * Build resolving property injection declaration. |
||
540 | * |
||
541 | * @param string $propertyName Target property name |
||
542 | * @param string $dependency Dependency class name |
||
543 | * @param string $containerVariable Container declaration variable name |
||
544 | * @param string $reflectionVariable Reflection class variable name |
||
545 | * @param bool $isPublic Flag if property is public |
||
546 | */ |
||
547 | 8 | protected function buildResolverPropertyDeclaration( |
|
579 | |||
580 | /** |
||
581 | * Build resolving method injection declaration. |
||
582 | * |
||
583 | * @param array $dependencies Collection of method dependencies |
||
584 | * @param string $methodName Method name |
||
585 | * @param string $containerVariable Container declaration variable name |
||
586 | * @param string $reflectionVariable Reflection class variable name |
||
587 | * @param bool $isPublic Flag if method is public |
||
588 | */ |
||
589 | 8 | protected function buildResolverMethodDeclaration( |
|
627 | } |
||
628 |