Completed
Push — master ( 3ad188...f91481 )
by Vitaly
09:32
created

Builder::buildReflectionClass()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 34
Code Lines 16

Duplication

Lines 20
Ratio 58.82 %

Code Coverage

Tests 16
CRAP Score 5

Importance

Changes 0
Metric Value
dl 20
loc 34
ccs 16
cts 16
cp 1
rs 8.439
c 0
b 0
f 0
cc 5
eloc 16
nc 7
nop 4
crap 5
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 implements ContainerBuilderInterface
22
{
23
    /** Controller classes scope name */
24
    const SCOPE_CONTROLLER = 'controllers';
25
26
    /** Service classes scope name */
27
    const SCOPE_SERVICES = 'service';
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 . 'Instances';
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
0 ignored issues
show
Bug introduced by
There is no parameter named $classMetadata. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

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