Completed
Push — master ( e845d8...40187b )
by Vitaly
03:03
created

MetadataBuilder   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 498
Duplicated Lines 4.02 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 98.91%

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 6
dl 20
loc 498
ccs 182
cts 184
cp 0.9891
rs 7.0642
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A loadFromPaths() 0 10 2
A loadFromClassNames() 0 14 3
B getDefinedClasses() 0 20 7
A loadFromCode() 0 10 2
B build() 0 38 2
A buildDependencyResolver() 0 14 1
F generateConditions() 20 143 22
A buildResolverCondition() 0 11 3
A buildResolvingClassDeclaration() 0 4 1
A buildResolverArgument() 0 12 4
B buildResolverPropertyDeclaration() 0 32 2
B buildResolverMethodDeclaration() 0 38 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

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 Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MetadataBuilder 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 MetadataBuilder, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types = 1);
2
/**
3
 * Created by PhpStorm.
4
 * User: root
5
 * Date: 02.08.16
6
 * Time: 0:46.
7
 */
8
namespace samsonframework\container;
9
10
use samsonframework\container\metadata\ClassMetadata;
11
use samsonframework\container\metadata\MethodMetadata;
12
use samsonframework\container\metadata\PropertyMetadata;
13
use samsonframework\container\resolver\ResolverInterface;
14
use samsonframework\di\Container;
15
use samsonframework\filemanager\FileManagerInterface;
16
use samsonphp\generator\Generator;
17
18
/**
19
 * Class Container.
20
 */
21
class MetadataBuilder
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 FileManagerInterface */
45
    protected $fileManger;
46
47
    /** @var ResolverInterface */
48
    protected $classResolver;
49
50
    /** @var Generator */
51
    protected $generator;
52
53
    /** @var string Resolver function name */
54
    protected $resolverFunction;
55
56
    /**
57
     * Container constructor.
58
     *
59
     * @param FileManagerInterface $fileManger
60
     * @param ResolverInterface    $classResolver
61
     * @param Generator            $generator
62
     */
63 10
    public function __construct(
64
        FileManagerInterface $fileManger,
65
        ResolverInterface $classResolver,
66
        Generator $generator
67
    )
68
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
69 10
        $this->fileManger = $fileManger;
70 10
        $this->classResolver = $classResolver;
71 10
        $this->generator = $generator;
72 10
    }
73
74
    /**
75
     * Load classes from paths.
76
     *
77
     * @param array $paths Paths for importing
78
     *
79
     * @return $this
80
     */
81 1
    public function loadFromPaths(array $paths)
82
    {
83
        // Iterate all paths and get files
84 1
        foreach ($this->fileManger->scan($paths, ['php']) as $phpFile) {
85
            // Read all classes in given file
86 1
            $this->loadFromClassNames($this->getDefinedClasses(require_once($phpFile)));
87
        }
88
89 1
        return $this;
90
    }
91
92
    /**
93
     * Load classes from class names collection.
94
     *
95
     * @param string[] $classes Collection of class names for resolving
96
     *
97
     * @return $this
98
     */
99 10
    public function loadFromClassNames(array $classes)
100
    {
101
        // Read all classes in given file
102 10
        foreach ($classes as $className) {
103
            // Resolve class metadata
104 9
            $this->classMetadata[$className] = $this->classResolver->resolve(new \ReflectionClass($className));
105
            // Store class in defined scopes
106 9
            foreach ($this->classMetadata[$className]->scopes as $scope) {
107 9
                $this->scopes[$scope][] = $className;
108
            }
109
        }
110
111 10
        return $this;
112
    }
113
114
    /**
115
     * Find class names defined in PHP code.
116
     *
117
     * @param string $php PHP code for scanning
118
     *
119
     * @return string[] Collection of found class names in php code
120
     */
121 2
    protected function getDefinedClasses($php) : array
122
    {
123 2
        $classes = array();
124
125
        // Append php marker for parsing file
126 2
        $php = strpos(is_string($php) ? $php : '', '<?php') !== 0 ? '<?php ' . $php : $php;
127
128 2
        $tokens = token_get_all($php);
129
130 2
        for ($i = 2, $count = count($tokens); $i < $count; $i++) {
131 1
            if ($tokens[$i - 2][0] === T_CLASS
132 1
                && $tokens[$i - 1][0] === T_WHITESPACE
133 1
                && $tokens[$i][0] === T_STRING
134
            ) {
135 1
                $classes[] = $tokens[$i][1];
136
            }
137
        }
138
139 2
        return $classes;
140
    }
141
142
    /**
143
     * Load classes from PHP code.
144
     *
145
     * @param string $php PHP code
146
     *
147
     * @return $this
148
     */
149 1
    public function loadFromCode($php)
150
    {
151 1
        if (count($classes = $this->getDefinedClasses($php))) {
152
            // TODO: Consider writing cache file and require it
153 1
            eval($php);
154 1
            $this->loadFromClassNames($classes);
155
        }
156
157 1
        return $this;
158
    }
159
160
    /**
161
     * Build container class.
162
     *
163
     * @param string|null $containerClass Container class name
164
     * @param string      $namespace      Name space
165
     *
166
     * @return string Generated Container class code
167
     * @throws \InvalidArgumentException
168
     */
169 7
    public function build($containerClass = 'Container', $namespace = '')
170
    {
171
        // Build dependency injection container function name
172 7
        $this->resolverFunction = uniqid(self::DI_FUNCTION_PREFIX);
173
174 7
        $this->generator
175 7
            ->text('<?php declare(strict_types = 1);')
176 7
            ->newLine()
177 7
            ->defNamespace($namespace)
178 7
            ->multiComment(['Application container'])
179 7
            ->defClass($containerClass, '\\' . Container::class)
180 7
            ->commentVar('array', 'Loaded dependencies')
181 7
            ->defClassVar('$dependencies', 'protected', array_keys($this->classMetadata))
182 7
            ->commentVar('array', 'Loaded services')
183 7
            ->defClassVar('$' . self::SCOPE_SERVICES, 'protected', $this->scopes[self::SCOPE_SERVICES])
184 7
            ->defClassFunction('logic', 'protected', ['$dependency'], ['Overridden dependency resolving function'])
185 7
            ->newLine('return $this->' . $this->resolverFunction . '($dependency);')
186 7
            ->endClassFunction();
187
188 7
        foreach ($this->classMetadata as $className => $classMetadata) {
189 7
            $dependencyName = $classMetadata->name ?? $className;
190
191
            // Generate camel case getter method
192 7
            $camelMethodName = 'get' . str_replace(' ', '', ucwords(ucfirst(str_replace(['\\', '_'], ' ', $dependencyName))));
193
194 7
            $this->generator
195 7
                ->defClassFunction($camelMethodName, 'public', [], ['@return \\' . $dependencyName . ' Get ' . $dependencyName . ' instance'])
196 7
                ->newLine('return $this->' . $this->resolverFunction . '(\'' . $dependencyName . '\');')
197 7
                ->endClassFunction();
198
        }
199
200
        // Build di container function and add to container class and return class code
201 7
        $this->buildDependencyResolver($this->resolverFunction);
202
203 7
        return $this->generator
204 7
            ->endClass()
205 7
            ->flush();
206
    }
207
208
    /**
209
     * Build dependency resolving function.
210
     *
211
     * @param string $functionName Function name
212
     *
213
     * @throws \InvalidArgumentException
214
     */
215 7
    protected function buildDependencyResolver($functionName)
216
    {
217 7
        $inputVariable = '$aliasOrClassName';
218 7
        $this->generator
219 7
            ->defClassFunction($functionName, 'protected', [$inputVariable], ['Dependency resolving function'])
220 7
            ->defVar('static ' . self::DI_FUNCTION_SERVICES . ' = []')
221 7
            ->newLine();
222
223
        // Generate all container and delegate conditions
224 7
        $this->generateConditions($inputVariable, false);
225
226
        // Add method not found
227 7
        $this->generator->endIfCondition()->endFunction();
228 7
    }
229
230
    /**
231
     * Generate logic conditions and their implementation for container and its delegates.
232
     *
233
     * @param string     $inputVariable Input condition parameter variable name
234
     * @param bool|false $started       Flag if condition branching has been started
235
     */
236 7
    public function generateConditions($inputVariable = '$alias', $started = false)
237
    {
238
        // Iterate all container dependencies
239 7
        foreach ($this->classMetadata as $className => $classMetadata) {
240
            // Generate condition statement to define if this class is needed
241 7
            $conditionFunc = !$started ? 'defIfCondition' : 'defElseIfCondition';
242
243
            // Output condition branch
244 7
            $this->generator->$conditionFunc(
245 7
                $this->buildResolverCondition($inputVariable, $className, $classMetadata->name)
246
            );
247
248
            // Define if this class has service scope
249 7
            $isService = in_array($className, $this->scopes[self::SCOPE_SERVICES], true);
250
251
            // Define class or service variable
252 7
            $staticContainerName = $isService
253 7
                ? self::DI_FUNCTION_SERVICES . '[\'' . $classMetadata->name . '\']'
254 7
                : '$temp';
255
256 7
            if ($isService) {
257
                // Check if dependency was instantiated
258 7
                $this->generator->defIfCondition('!array_key_exists(\'' . $className . '\', ' . self::DI_FUNCTION_SERVICES . ')');
259
            }
260
261 7
            $this->generator->newLine($staticContainerName . ' = ');
262 7
            $this->buildResolvingClassDeclaration($className);
263
264
            // Process constructor dependencies
265 7
            $argumentsCount = 0;
266 7
            if (array_key_exists('__construct', $classMetadata->methodsMetadata)) {
267 7
                $constructorArguments = $classMetadata->methodsMetadata['__construct']->dependencies;
268 7
                $argumentsCount = count($constructorArguments);
269 7
                $i = 0;
270
271
                // Add indentation to move declaration arguments
272 7
                $this->generator->tabs++;
273
274
                // Process constructor arguments
275 7
                foreach ($constructorArguments as $argument => $dependency) {
276 7
                    $this->buildResolverArgument($dependency);
277
278
                    // Add comma if this is not last dependency
279 7
                    if (++$i < $argumentsCount) {
280 7
                        $this->generator->text(',');
281
                    }
282
                }
283
284
                // Restore indentation
285 7
                $this->generator->tabs--;
286
            }
287
288
            // Close declaration block, multiline if we have dependencies
289 7
            $argumentsCount ? $this->generator->newLine(');') : $this->generator->text(');');
290 7
            $this->generator->newLine();
291
292
            // Internal scope reflection variable
293 7
            $reflectionVariable = '$reflectionClass';
294
295
            /** @var MethodMetadata[] Gather only valid method for container */
296 7
            $classValidMethods = [];
297 7
            foreach ($classMetadata->methodsMetadata as $methodName => $methodMetadata) {
298
                // Skip constructor method and empty dependencies
299 7
                if ($methodName !== '__construct' && count($methodMetadata->dependencies) > 0) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $methodName (integer) and '__construct' (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
300 7
                    $classValidMethods[$methodName] = $methodMetadata;
301
                }
302
            }
303
304
            /** @var PropertyMetadata[] Gather only valid property for container */
305 7
            $classValidProperties = [];
306 7
            foreach ($classMetadata->propertiesMetadata as $propertyName => $propertyMetadata) {
307
                // Skip constructor method and empty dependencies
308 7
                if ($propertyMetadata->dependency) {
309 7
                    $classValidProperties[$propertyName] = $propertyMetadata;
310
                }
311
            }
312
313
            /**
314
             * Iterate all properties and create internal scope reflection class instance if
315
             * at least one property in not public
316
             */
317 7 View Code Duplication
            foreach ($classValidProperties as $propertyMetadata) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
318 7
                if (!$propertyMetadata->isPublic) {
319 7
                    $this->generator
320 7
                        ->comment('Create reflection class for injecting private/protected properties and methods')
321 7
                        ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
322 7
                        ->newLine();
323
324 7
                    break;
325
                }
326
            }
327
328
            /**
329
             * Iterate all properties and create internal scope reflection class instance if
330
             * at least one property in not public
331
             */
332 7 View Code Duplication
            foreach ($classValidMethods as $methodMetadata) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
333 7
                if (!$methodMetadata->isPublic) {
334 7
                    $this->generator
335 7
                        ->comment('Create reflection class for injecting private/protected properties and methods')
336 7
                        ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
337 7
                        ->newLine();
338
339 7
                    break;
340
                }
341
            }
342
343
            // Process class properties
344 7
            foreach ($classValidProperties as $property) {
345
                // If such property has the dependency
346 7
                if ($property->dependency) {
347
                    // Set value via refection
348 7
                    $this->buildResolverPropertyDeclaration(
349 7
                        $property->name,
350 7
                        $property->dependency,
351
                        $staticContainerName,
352
                        $reflectionVariable,
353 7
                        $property->isPublic
354
                    );
355
                }
356
            }
357
358
            /** @var MethodMetadata $methodMetadata */
359 7
            foreach ($classValidMethods as $methodName => $methodMetadata) {
360 7
                $this->buildResolverMethodDeclaration(
361 7
                    $methodMetadata->dependencies,
362
                    $methodName,
363
                    $staticContainerName,
364
                    $reflectionVariable,
365 7
                    $methodMetadata->isPublic
366
                );
367
            }
368
369 7
            if ($isService) {
370 7
                $this->generator->endIfCondition();
371
            }
372
373 7
            $this->generator->newLine()->newLine('return ' . $staticContainerName . ';');
374
375
            // Set flag that condition is started
376 7
            $started = true;
377
        }
378 7
    }
379
380
    /**
381
     * Build resolving function condition.
382
     *
383
     * @param string      $inputVariable Condition variable
384
     * @param string      $className
385
     * @param string|null $alias
386
     *
387
     * @return string Condition code
388
     */
389 7
    protected function buildResolverCondition(string $inputVariable, string $className, string $alias = null) : string
390
    {
391
        // Create condition branch
392 7
        $condition = $inputVariable . ' === \'' . $className . '\'';
393
394 7
        if ($alias !== null && $alias !== $className) {
395 7
            $condition .= '||' . $this->buildResolverCondition($inputVariable, $alias);
396
        }
397
398 7
        return $condition;
399
    }
400
401
    /**
402
     * Build resolving function class block.
403
     *
404
     * @param string $className Class name for new instance creation
405
     */
406 7
    protected function buildResolvingClassDeclaration(string $className)
407
    {
408 7
        $this->generator->text('new \\' . ltrim($className, '\\') . '(');
409 7
    }
410
411
    /**
412
     * Build resolving function dependency argument.
413
     *
414
     * @param mixed $argument Dependency argument
415
     */
416 7
    protected function buildResolverArgument($argument, $textFunction = 'newLine')
417
    {
418
        // This is a dependency which invokes resolving function
419 7
        if (array_key_exists($argument, $this->classMetadata)) {
420
            // Call container logic for this dependency
421 7
            $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
422 1
        } elseif (is_string($argument)) { // String variable
423 1
            $this->generator->$textFunction()->stringValue($argument);
424
        } elseif (is_array($argument)) { // Dependency value is array
425
            $this->generator->$textFunction()->arrayValue($argument);
426
        }
427 7
    }
428
429
    /**
430
     * Build resolving property injection declaration.
431
     *
432
     * @param string $propertyName       Target property name
433
     * @param string $dependency         Dependency class name
434
     * @param string $containerVariable  Container declaration variable name
435
     * @param string $reflectionVariable Reflection class variable name
436
     * @param bool   $isPublic           Flag if property is public
437
     */
438 7
    protected function buildResolverPropertyDeclaration(
439
        string $propertyName,
440
        string $dependency,
441
        string $containerVariable,
442
        string $reflectionVariable,
443
        bool $isPublic
444
    )
445
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
446 7
        if ($isPublic) {
447 7
            $this->generator
448 7
                ->comment('Inject public dependency for $' . $propertyName)
449 7
                ->newLine($containerVariable . '->' . $propertyName . ' = ');
450 7
            $this->buildResolverArgument($dependency, 'text');
451 7
            $this->generator->text(';');
452
        } else {
453 7
            $this->generator
0 ignored issues
show
Bug introduced by
The method increaseIndentation() does not seem to exist on object<samsonphp\generator\Generator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
454 7
                ->comment('Inject private dependency for $' . $propertyName)
455 7
                ->newLine('$property = ' . $reflectionVariable . '->getProperty(\'' . $propertyName . '\');')
456 7
                ->newLine('$property->setAccessible(true);')
457 7
                ->newLine('$property->setValue(')
458 7
                ->increaseIndentation()
459 7
                ->newLine($containerVariable . ',');
460
461 7
            $this->buildResolverArgument($dependency);
462
463 7
            $this->generator
0 ignored issues
show
Bug introduced by
The method decreaseIndentation() does not seem to exist on object<samsonphp\generator\Generator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
464 7
                ->decreaseIndentation()
465 7
                ->newLine(');')
466 7
                ->newLine('$property->setAccessible(false);')
467 7
                ->newLine();
468
        }
469 7
    }
470
471
    /**
472
     * Build resolving method injection declaration.
473
     *
474
     * @param array  $dependencies       Collection of method dependencies
475
     * @param string $methodName         Method name
476
     * @param string $containerVariable  Container declaration variable name
477
     * @param string $reflectionVariable Reflection class variable name
478
     * @param bool   $isPublic           Flag if method is public
479
     */
480 7
    protected function buildResolverMethodDeclaration(
481
        array $dependencies,
482
        string $methodName,
483
        string $containerVariable,
484
        string $reflectionVariable,
485
        bool $isPublic
486
    )
487
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
488
        // Get method arguments
489 7
        $argumentsCount = count($dependencies);
490
491 7
        $this->generator->comment('Invoke ' . $methodName . '() and pass dependencies(y)');
492
493 7
        if ($isPublic) {
494 7
            $this->generator->newLine($containerVariable . '->' . $methodName . '(')->increaseIndentation();
0 ignored issues
show
Bug introduced by
The method increaseIndentation() does not seem to exist on object<samsonphp\generator\Generator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
495
        } else {
496 7
            $this->generator
0 ignored issues
show
Bug introduced by
The method increaseIndentation() does not seem to exist on object<samsonphp\generator\Generator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
497 7
                ->newLine('$method = ' . $reflectionVariable . '->getMethod(\'' . $methodName . '\');')
498 7
                ->newLine('$method->setAccessible(true);')
499 7
                ->newLine('$method->invoke(')
500 7
                ->increaseIndentation()
501 7
                ->newLine($containerVariable . ',');
502
        }
503
504 7
        $i = 0;
505
        // Iterate method arguments
506 7
        foreach ($dependencies as $argument => $dependency) {
507
            // Add dependencies
508 7
            $this->buildResolverArgument($dependency);
509
510
            // Add comma if this is not last dependency
511 7
            if (++$i < $argumentsCount) {
512 7
                $this->generator->text(',');
513
            }
514
        }
515
516 7
        $this->generator->decreaseIndentation()->newLine(');');
0 ignored issues
show
Bug introduced by
The method decreaseIndentation() does not seem to exist on object<samsonphp\generator\Generator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
517 7
    }
518
}
519