Completed
Push — master ( 729a7d...208a15 )
by Vitaly
03:04
created

Builder::generateConditions()   C

Complexity

Conditions 12
Paths 33

Size

Total Lines 88
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 12.0022

Importance

Changes 0
Metric Value
dl 0
loc 88
ccs 39
cts 40
cp 0.975
rs 5.034
c 0
b 0
f 0
cc 12
eloc 45
nc 33
nop 2
crap 12.0022

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 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\di\Container;
14
use samsonphp\generator\Generator;
15
16
/**
17
 * Container builder.
18
 *
19
 * @author Vitaly Egorov <[email protected]>
20
 */
21
class Builder
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 $classesMetadata = [];
43
44
    /** @var array Collection of dependencies aliases */
45
    protected $classAliases = [];
46
47
    /**
48
     * @var Generator
49
     * @Injectable
50
     */
51
    protected $generator;
52
53
    /** @var string Resolver function name */
54
    protected $resolverFunction;
55
56
    /**
57
     * Container builder constructor.
58
     *
59
     * @param Generator       $generator     PHP code generator
60
     * @param ClassMetadata[] $classMetadata Collection of classes metadata for container
61
     */
62 1
    public function __construct(Generator $generator, array $classMetadata)
63
    {
64 1
        $this->generator = $generator;
65 1
        $this->classesMetadata = $classMetadata;
66
67 1
        $this->processClassMetadata($this->classesMetadata);
68 1
    }
69
70
    /**
71
     * Read class metadata and fill internal collections.
72
     *
73
     * @param ClassMetadata[] $classesMetadata
74
     */
75 1
    public function processClassMetadata(array $classesMetadata)
76
    {
77
        // Read all classes in given file
78 1
        foreach ($classesMetadata as $classMetadata) {
79
            // Store by metadata name as alias
80 1
            $this->classAliases[$classMetadata->name] = $classMetadata->className;
81
82
            // Store class in defined scopes
83 1
            foreach ($classMetadata->scopes as $scope) {
84 1
                $this->scopes[$scope][] = $classMetadata->className;
85
            }
86
        }
87 1
    }
88
89
    /**
90
     * Build container class.
91
     *
92
     * @param string|null $containerClass Container class name
93
     * @param string      $namespace      Name space
94
     *
95
     * @return string Generated Container class code
96
     * @throws \InvalidArgumentException
97
     */
98 1
    public function build($containerClass = 'Container', $namespace = '')
99
    {
100
        // Build dependency injection container function name
101 1
        $this->resolverFunction = uniqid(self::DI_FUNCTION_PREFIX);
102
103 1
        $containerDependencies = [];
104 1
        $containerAliases = [];
105 1
        foreach ($this->classesMetadata as $classMetadata) {
106 1
            $className = $classMetadata->className;
107 1
            if ($classMetadata->alias !== null) {
108
                $containerAliases[$className] = $classMetadata->alias;
109
            }
110
            // Store inner dependencies
111 1
            if (array_key_exists('__construct', $classMetadata->methodsMetadata)) {
112 1
                $containerDependencies[$className] = array_values($classMetadata->methodsMetadata['__construct']->dependencies ?? []);
113
            }
114
        }
115
116 1
        $this->generator
117 1
            ->text('<?php declare(strict_types = 1);')
118 1
            ->newLine()
119 1
            ->defNamespace($namespace)
120 1
            ->multiComment(['Application container'])
121 1
            ->defClass($containerClass, '\\' . Container::class)
122 1
            ->defClassFunction('__construct')
123 1
            ->newLine('$this->dependencies = ')->arrayValue($containerDependencies)->text(';')
124 1
            ->newLine('$this->aliases = ')->arrayValue($containerAliases)->text(';')
125 1
            ->newLine('$this->' . self::SCOPE_SERVICES . ' = ')->arrayValue($this->scopes[self::SCOPE_SERVICES])->text(';')
0 ignored issues
show
Documentation introduced by
$this->scopes[self::SCOPE_SERVICES] is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
126 1
            ->endClassFunction()
127 1
            ->defClassFunction('logic', 'protected', ['$dependency'], ['{@inheritdoc}'])
128 1
            ->newLine('return $this->' . $this->resolverFunction . '($dependency);')
129 1
            ->endClassFunction();
130
131 1
        foreach ($this->classesMetadata as $classMetadata) {
132 1
            $className = $classMetadata->className;
133 1
            $dependencyName = $classMetadata->name ?? $className;
134
135
            // Generate camel case getter method
136 1
            $camelMethodName = 'get' . str_replace(' ', '', ucwords(ucfirst(str_replace(['\\', '_'], ' ', $dependencyName))));
137
138 1
            $this->generator
139 1
                ->defClassFunction($camelMethodName, 'public', [], ['@return \\' . $className . ' Get ' . $dependencyName . ' instance'])
140 1
                ->newLine('return $this->' . $this->resolverFunction . '(\'' . $dependencyName . '\');')
141 1
                ->endClassFunction();
142
        }
143
144
        // Build di container function and add to container class and return class code
145 1
        $this->buildDependencyResolver($this->resolverFunction);
146
147 1
        return $this->generator
148 1
            ->endClass()
149 1
            ->flush();
150
    }
151
152
    /**
153
     * Build dependency resolving function.
154
     *
155
     * @param string $functionName Function name
156
     *
157
     * @throws \InvalidArgumentException
158
     */
159 1
    protected function buildDependencyResolver($functionName)
160
    {
161 1
        $inputVariable = '$aliasOrClassName';
162 1
        $this->generator
163 1
            ->defClassFunction($functionName, 'protected', [$inputVariable], ['Dependency resolving function'])
164 1
            ->defVar('static ' . self::DI_FUNCTION_SERVICES . ' = []')
165 1
            ->newLine();
166
167
        // Generate all container and delegate conditions
168 1
        $this->generateConditions($inputVariable, false);
169
170
        // Add method not found
171 1
        $this->generator->endIfCondition()->endFunction();
172 1
    }
173
174
    /**
175
     * Generate logic conditions and their implementation for container and its delegates.
176
     *
177
     * @param string     $inputVariable Input condition parameter variable name
178
     * @param bool|false $started       Flag if condition branching has been started
179
     */
180 1
    public function generateConditions($inputVariable = '$alias', $started = false)
181
    {
182
        // Iterate all container dependencies
183 1
        foreach ($this->classesMetadata as $classMetadata) {
184 1
            $className = $classMetadata->className;
185
            // Generate condition statement to define if this class is needed
186 1
            $conditionFunc = !$started ? 'defIfCondition' : 'defElseIfCondition';
187
188
            // Output condition branch
189 1
            $this->generator->$conditionFunc(
190 1
                $this->buildResolverCondition($inputVariable, $className, $classMetadata->name)
191
            );
192
193
            // Define if this class has service scope
194 1
            $isService = in_array($className, $this->scopes[self::SCOPE_SERVICES], true);
195
196
            /** @var MethodMetadata[] Gather only valid method for container */
197 1
            $classValidMethods = $this->getValidClassMethodsMetadata($classMetadata->methodsMetadata);
198
199
            /** @var PropertyMetadata[] Gather only valid property for container */
200 1
            $classValidProperties = $this->getValidClassPropertiesMetadata($classMetadata->propertiesMetadata);
201
202
            // Define class or service variable
203 1
            $staticContainerName = $isService
204 1
                ? self::DI_FUNCTION_SERVICES . '[\'' . $classMetadata->name . '\']'
205 1
                : '$temp';
206
207 1
            if ($isService) {
208
                // Check if dependency was instantiated
209 1
                $this->generator->defIfCondition('!array_key_exists(\'' . $className . '\', ' . self::DI_FUNCTION_SERVICES . ')');
210
            }
211
212 1
            if (count($classValidMethods) || count($classValidProperties)) {
213 1
                $this->generator->newLine($staticContainerName . ' = ');
214 1
                $this->buildResolvingClassDeclaration($className);
215 1
                $this->buildConstructorDependencies($classMetadata->methodsMetadata);
216
217
                // Internal scope reflection variable
218 1
                $reflectionVariable = '$reflectionClass';
219
220 1
                $this->buildReflectionClass($className, $classValidProperties, $classValidMethods, $reflectionVariable);
221
222
                // Process class properties
223 1
                foreach ($classValidProperties as $property) {
224
                    // If such property has the dependency
225 1
                    if ($property->dependency) {
226
                        // Set value via refection
227 1
                        $this->buildResolverPropertyDeclaration(
228 1
                            $property->name,
229 1
                            $property->dependency,
230
                            $staticContainerName,
231
                            $reflectionVariable,
232 1
                            $property->isPublic
233
                        );
234
                    }
235
                }
236
237
                /** @var MethodMetadata $methodMetadata */
238 1
                foreach ($classValidMethods as $methodName => $methodMetadata) {
239 1
                    $this->buildResolverMethodDeclaration(
240 1
                        $methodMetadata->dependencies,
241
                        $methodName,
242
                        $staticContainerName,
243
                        $reflectionVariable,
244 1
                        $methodMetadata->isPublic
245
                    );
246
                }
247
248 1
                if ($isService) {
249 1
                    $this->generator->endIfCondition();
250
                }
251
252 1
                $this->generator->newLine()->newLine('return ' . $staticContainerName . ';');
253
            } else {
254 1
                $this->generator->newLine('return ');
255 1
                $this->buildResolvingClassDeclaration($className);
256 1
                $this->buildConstructorDependencies($classMetadata->methodsMetadata);
257
258 1
                if ($isService) {
259
                    $this->generator->endIfCondition()->newLine('return ' . $staticContainerName . ';');
260
                }
261
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
262
            }
263
264
            // Set flag that condition is started
265 1
            $started = true;
266
        }
267 1
    }
268
269
    /**
270
     * Build resolving function condition.
271
     *
272
     * @param string      $inputVariable Condition variable
273
     * @param string      $className
274
     * @param string|null $alias
275
     *
276
     * @return string Condition code
277
     */
278 1
    protected function buildResolverCondition(string $inputVariable, string $className, string $alias = null) : string
279
    {
280
        // Create condition branch
281 1
        $condition = $inputVariable . ' === \'' . $className . '\'';
282
283 1
        if ($alias !== null && $alias !== $className) {
284 1
            $condition .= '||' . $this->buildResolverCondition($inputVariable, $alias);
285
        }
286
287 1
        return $condition;
288
    }
289
290
    /**
291
     * Get valid class methods metadata.
292
     *
293
     * @param MethodMetadata[] $classMethodsMetadata All class methods metadata
294
     *
295
     * @return array Valid class methods metadata
296
     */
297 1
    protected function getValidClassMethodsMetadata(array $classMethodsMetadata)
298
    {
299
        /** @var MethodMetadata[] Gather only valid method for container */
300 1
        $classValidMethods = [];
301 1
        foreach ($classMethodsMetadata as $methodName => $methodMetadata) {
302
            // Skip constructor method and empty dependencies
303 1
            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...
304 1
                $classValidMethods[$methodName] = $methodMetadata;
305
            }
306
        }
307
308 1
        return $classValidMethods;
309
    }
310
311
    /**
312
     * Get valid class properties metadata.
313
     *
314
     * @param PropertyMetadata[] $classPropertiesMetadata All class properties metadata
315
     *
316
     * @return array Valid class properties metadata
317
     */
318 1
    protected function getValidClassPropertiesMetadata(array $classPropertiesMetadata)
319
    {
320
        /** @var PropertyMetadata[] Gather only valid property for container */
321 1
        $classValidProperties = [];
322 1
        foreach ($classPropertiesMetadata as $propertyName => $propertyMetadata) {
323
            // Skip constructor method and empty dependencies
324 1
            if ($propertyMetadata->dependency) {
325 1
                $classValidProperties[$propertyName] = $propertyMetadata;
326
            }
327
        }
328
329 1
        return $classValidProperties;
330
    }
331
332
    /**
333
     * Build resolving function class block.
334
     *
335
     * @param string $className Class name for new instance creation
336
     */
337 1
    protected function buildResolvingClassDeclaration(string $className)
338
    {
339 1
        $this->generator->text('new \\' . ltrim($className, '\\') . '(');
340 1
    }
341
342
    /**
343
     * Build constructor arguments injection.
344
     *
345
     * @param MethodMetadata[] $methodsMetaData
346
     */
347 1
    protected function buildConstructorDependencies(array $methodsMetaData)
348
    {
349
        // Process constructor dependencies
350 1
        $argumentsCount = 0;
351 1
        if (array_key_exists('__construct', $methodsMetaData)) {
352 1
            $constructorArguments = $methodsMetaData['__construct']->dependencies ?? [];
353 1
            $argumentsCount = count($constructorArguments);
354 1
            $i = 0;
355
356
            // Add indentation to move declaration arguments
357 1
            $this->generator->tabs++;
358
359
            // Process constructor arguments
360 1
            foreach ($constructorArguments as $argument => $dependency) {
361 1
                $this->buildResolverArgument($dependency);
362
363
                // Add comma if this is not last dependency
364 1
                if (++$i < $argumentsCount) {
365 1
                    $this->generator->text(',');
366
                }
367
            }
368
369
            // Restore indentation
370 1
            $this->generator->tabs--;
371
        }
372
373
        // Close declaration block, multiline if we have dependencies
374 1
        $argumentsCount ? $this->generator->newLine(');') : $this->generator->text(');');
375 1
    }
376
377
    /**
378
     * Build resolving function dependency argument.
379
     *
380
     * @param mixed $argument Dependency argument
381
     */
382 1
    protected function buildResolverArgument($argument, $textFunction = 'newLine')
383
    {
384
        // This is a dependency which invokes resolving function
385 1
        if (is_string($argument)) {
386 1
            if (array_key_exists($argument, $this->classesMetadata)) {
387
                // Call container logic for this dependency
388 1
                $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
389 1
            } elseif (array_key_exists($argument, $this->classAliases)) {
390
                // Call container logic for this dependency
391
                $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
392
            } else { // String variable
393 1
                $this->generator->$textFunction()->stringValue($argument);
394
            }
395 1
        } elseif (is_array($argument)) { // Dependency value is array
396 1
            $this->generator->$textFunction()->arrayValue($argument);
397
        }
398 1
    }
399
400
    /**
401
     * Generate reflection class for private/protected methods or properties
402
     * in current scope.
403
     *
404
     * @param string             $className          Reflection class source class name
405
     * @param PropertyMetadata[] $propertiesMetadata Properties metadata
406
     * @param MethodMetadata[]   $methodsMetadata    Methods metadata
407
     * @param string             $reflectionVariable Reflection class variable name
408
     */
409 1
    protected function buildReflectionClass(string $className, array $propertiesMetadata, array $methodsMetadata, string $reflectionVariable)
410
    {
411
        /**
412
         * Iterate all properties and create internal scope reflection class instance if
413
         * at least one property in not public
414
         */
415 1 View Code Duplication
        foreach ($propertiesMetadata 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...
416 1
            if (!$propertyMetadata->isPublic) {
417 1
                $this->generator
418 1
                    ->comment('Create reflection class for injecting private/protected properties and methods')
419 1
                    ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
420 1
                    ->newLine();
421
422 1
                return true;
423
            }
424
        }
425
426
        /**
427
         * Iterate all properties and create internal scope reflection class instance if
428
         * at least one property in not public
429
         */
430 1 View Code Duplication
        foreach ($methodsMetadata 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...
431 1
            if (!$methodMetadata->isPublic) {
432 1
                $this->generator
433 1
                    ->comment('Create reflection class for injecting private/protected properties and methods')
434 1
                    ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
435 1
                    ->newLine();
436
437 1
                return true;
438
            }
439
        }
440
441 1
        return false;
442
    }
443
444
    /**
445
     * Build resolving property injection declaration.
446
     *
447
     * @param string $propertyName       Target property name
448
     * @param string $dependency         Dependency class name
449
     * @param string $containerVariable  Container declaration variable name
450
     * @param string $reflectionVariable Reflection class variable name
451
     * @param bool   $isPublic           Flag if property is public
452
     */
453 1
    protected function buildResolverPropertyDeclaration(
454
        string $propertyName,
455
        string $dependency,
456
        string $containerVariable,
457
        string $reflectionVariable,
458
        bool $isPublic
459
    )
460
    {
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...
461 1
        if ($isPublic) {
462 1
            $this->generator
463 1
                ->comment('Inject public dependency for $' . $propertyName)
464 1
                ->newLine($containerVariable . '->' . $propertyName . ' = ');
465 1
            $this->buildResolverArgument($dependency, 'text');
466 1
            $this->generator->text(';');
467
        } else {
468 1
            $this->generator
469 1
                ->comment('Inject private dependency for $' . $propertyName)
470 1
                ->newLine('$property = ' . $reflectionVariable . '->getProperty(\'' . $propertyName . '\');')
471 1
                ->newLine('$property->setAccessible(true);')
472 1
                ->newLine('$property->setValue(')
473 1
                ->increaseIndentation()
474 1
                ->newLine($containerVariable . ',');
475
476 1
            $this->buildResolverArgument($dependency);
477
478 1
            $this->generator
479 1
                ->decreaseIndentation()
480 1
                ->newLine(');')
481 1
                ->newLine('$property->setAccessible(false);')
482 1
                ->newLine();
483
        }
484 1
    }
485
486
    /**
487
     * Build resolving method injection declaration.
488
     *
489
     * @param array  $dependencies       Collection of method dependencies
490
     * @param string $methodName         Method name
491
     * @param string $containerVariable  Container declaration variable name
492
     * @param string $reflectionVariable Reflection class variable name
493
     * @param bool   $isPublic           Flag if method is public
494
     */
495 1
    protected function buildResolverMethodDeclaration(
496
        array $dependencies,
497
        string $methodName,
498
        string $containerVariable,
499
        string $reflectionVariable,
500
        bool $isPublic
501
    )
502
    {
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...
503
        // Get method arguments
504 1
        $argumentsCount = count($dependencies);
505
506 1
        $this->generator->comment('Invoke ' . $methodName . '() and pass dependencies(y)');
507
508 1
        if ($isPublic) {
509 1
            $this->generator->newLine($containerVariable . '->' . $methodName . '(')->increaseIndentation();
510
        } else {
511 1
            $this->generator
512 1
                ->newLine('$method = ' . $reflectionVariable . '->getMethod(\'' . $methodName . '\');')
513 1
                ->newLine('$method->setAccessible(true);')
514 1
                ->newLine('$method->invoke(')
515 1
                ->increaseIndentation()
516 1
                ->newLine($containerVariable . ',');
517
        }
518
519 1
        $i = 0;
520
        // Iterate method arguments
521 1
        foreach ($dependencies as $argument => $dependency) {
522
            // Add dependencies
523 1
            $this->buildResolverArgument($dependency);
524
525
            // Add comma if this is not last dependency
526 1
            if (++$i < $argumentsCount) {
527 1
                $this->generator->text(',');
528
            }
529
        }
530
531 1
        $this->generator->decreaseIndentation()->newLine(');');
532 1
    }
533
}
534