Completed
Push — master ( e5e64a...df11fd )
by Vitaly
02:42
created

ContainerBuilder::buildConstructorDependencies()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 29
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 14
cts 14
cp 1
rs 8.439
c 0
b 0
f 0
cc 5
eloc 13
nc 4
nop 1
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\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 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 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
            /** @var MethodMetadata[] Gather only valid method for container */
252 7
            $classValidMethods = $this->getValidClassMethodsMetadata($classMetadata->methodsMetadata);
253
254
            /** @var PropertyMetadata[] Gather only valid property for container */
255 7
            $classValidProperties = $this->getValidClassPropertiesMetadata($classMetadata->propertiesMetadata);
256
257
            // Define class or service variable
258 7
            $staticContainerName = $isService
259 7
                ? self::DI_FUNCTION_SERVICES . '[\'' . $classMetadata->name . '\']'
260 7
                : '$temp';
261
262 7
            if ($isService) {
263
                // Check if dependency was instantiated
264 7
                $this->generator->defIfCondition('!array_key_exists(\'' . $className . '\', ' . self::DI_FUNCTION_SERVICES . ')');
265
            }
266
267 7
            if (count($classValidMethods) || count($classValidProperties)) {
268 7
                $this->generator->newLine($staticContainerName . ' = ');
269 7
                $this->buildResolvingClassDeclaration($className);
270 7
                $this->buildConstructorDependencies($classMetadata->methodsMetadata);
271
272
                // Internal scope reflection variable
273 7
                $reflectionVariable = '$reflectionClass';
274
275 7
                $this->buildReflectionClass($className, $classValidProperties, $classValidMethods, $reflectionVariable);
276
277
                // Process class properties
278 7
                foreach ($classValidProperties as $property) {
279
                    // If such property has the dependency
280 7
                    if ($property->dependency) {
281
                        // Set value via refection
282 7
                        $this->buildResolverPropertyDeclaration(
283 7
                            $property->name,
284 7
                            $property->dependency,
285
                            $staticContainerName,
286
                            $reflectionVariable,
287 7
                            $property->isPublic
288
                        );
289
                    }
290
                }
291
292
                /** @var MethodMetadata $methodMetadata */
293 7
                foreach ($classValidMethods as $methodName => $methodMetadata) {
294 7
                    $this->buildResolverMethodDeclaration(
295 7
                        $methodMetadata->dependencies,
296
                        $methodName,
297
                        $staticContainerName,
298
                        $reflectionVariable,
299 7
                        $methodMetadata->isPublic
300
                    );
301
                }
302
303 7
                if ($isService) {
304 7
                    $this->generator->endIfCondition();
305
                }
306
307 7
                $this->generator->newLine()->newLine('return ' . $staticContainerName . ';');
308
            } else {
309 7
                $this->generator->newLine('return ' . $staticContainerName . ' = ');
310 7
                $this->buildResolvingClassDeclaration($className);
311 7
                $this->buildConstructorDependencies($classMetadata->methodsMetadata);
312
            }
313
314
            // Set flag that condition is started
315 7
            $started = true;
316
        }
317 7
    }
318
319
    /**
320
     * Build resolving function condition.
321
     *
322
     * @param string      $inputVariable Condition variable
323
     * @param string      $className
324
     * @param string|null $alias
325
     *
326
     * @return string Condition code
327
     */
328 7
    protected function buildResolverCondition(string $inputVariable, string $className, string $alias = null) : string
329
    {
330
        // Create condition branch
331 7
        $condition = $inputVariable . ' === \'' . $className . '\'';
332
333 7
        if ($alias !== null && $alias !== $className) {
334 7
            $condition .= '||' . $this->buildResolverCondition($inputVariable, $alias);
335
        }
336
337 7
        return $condition;
338
    }
339
340
    /**
341
     * Get valid class methods metadata.
342
     *
343
     * @param MethodMetadata[] $classMethodsMetadata All class methods metadata
344
     *
345
     * @return array Valid class methods metadata
346
     */
347 7
    protected function getValidClassMethodsMetadata(array $classMethodsMetadata)
348
    {
349
        /** @var MethodMetadata[] Gather only valid method for container */
350 7
        $classValidMethods = [];
351 7
        foreach ($classMethodsMetadata as $methodName => $methodMetadata) {
352
            // Skip constructor method and empty dependencies
353 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...
354 7
                $classValidMethods[$methodName] = $methodMetadata;
355
            }
356
        }
357
358 7
        return $classValidMethods;
359
    }
360
361
    /**
362
     * Get valid class properties metadata.
363
     *
364
     * @param PropertyMetadata[] $classPropertiesMetadata All class properties metadata
365
     *
366
     * @return array Valid class properties metadata
367
     */
368 7
    protected function getValidClassPropertiesMetadata(array $classPropertiesMetadata)
369
    {
370
        /** @var PropertyMetadata[] Gather only valid property for container */
371 7
        $classValidProperties = [];
372 7
        foreach ($classPropertiesMetadata as $propertyName => $propertyMetadata) {
373
            // Skip constructor method and empty dependencies
374 7
            if ($propertyMetadata->dependency) {
375 7
                $classValidProperties[$propertyName] = $propertyMetadata;
376
            }
377
        }
378
379 7
        return $classValidProperties;
380
    }
381
382
    /**
383
     * Build resolving function class block.
384
     *
385
     * @param string $className Class name for new instance creation
386
     */
387 7
    protected function buildResolvingClassDeclaration(string $className)
388
    {
389 7
        $this->generator->text('new \\' . ltrim($className, '\\') . '(');
390 7
    }
391
392
    /**
393
     * Build constructor arguments injection.
394
     *
395
     * @param MethodMetadata[] $methodsMetaData
396
     */
397 7
    protected function buildConstructorDependencies(array $methodsMetaData)
398
    {
399
        // Process constructor dependencies
400 7
        $argumentsCount = 0;
401 7
        if (array_key_exists('__construct', $methodsMetaData)) {
402 7
            $constructorArguments = $methodsMetaData['__construct']->dependencies;
403 7
            $argumentsCount = count($constructorArguments);
404 7
            $i = 0;
405
406
            // Add indentation to move declaration arguments
407 7
            $this->generator->tabs++;
408
409
            // Process constructor arguments
410 7
            foreach ($constructorArguments as $argument => $dependency) {
411 7
                $this->buildResolverArgument($dependency);
412
413
                // Add comma if this is not last dependency
414 7
                if (++$i < $argumentsCount) {
415 7
                    $this->generator->text(',');
416
                }
417
            }
418
419
            // Restore indentation
420 7
            $this->generator->tabs--;
421
        }
422
423
        // Close declaration block, multiline if we have dependencies
424 7
        $argumentsCount ? $this->generator->newLine(');') : $this->generator->text(');');
425 7
    }
426
427
    /**
428
     * Build resolving function dependency argument.
429
     *
430
     * @param mixed $argument Dependency argument
431
     */
432 7
    protected function buildResolverArgument($argument, $textFunction = 'newLine')
433
    {
434
        // This is a dependency which invokes resolving function
435 7
        if (array_key_exists($argument, $this->classMetadata)) {
436
            // Call container logic for this dependency
437 7
            $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
438 1
        } elseif (is_string($argument)) { // String variable
439 1
            $this->generator->$textFunction()->stringValue($argument);
440
        } elseif (is_array($argument)) { // Dependency value is array
441
            $this->generator->$textFunction()->arrayValue($argument);
442
        }
443 7
    }
444
445
    /**
446
     * Generate reflection class for private/protected methods or properties
447
     * in current scope.
448
     *
449
     * @param string             $className          Reflection class source class name
450
     * @param PropertyMetadata[] $propertiesMetadata Properties metadata
451
     * @param MethodMetadata[]   $methodsMetadata    Methods metadata
452
     * @param string             $reflectionVariable Reflection class variable name
453
     */
454 7
    protected function buildReflectionClass(string $className, array $propertiesMetadata, array $methodsMetadata, string $reflectionVariable)
455
    {
456
        /** @var bool $reflectionClassCreated Flag showing that reflection class already created in current scope */
457 7
        $reflectionClassCreated = false;
458
459
        /**
460
         * Iterate all properties and create internal scope reflection class instance if
461
         * at least one property in not public
462
         */
463 7 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...
464 7
            if (!$propertyMetadata->isPublic) {
465 7
                $this->generator
466 7
                    ->comment('Create reflection class for injecting private/protected properties and methods')
467 7
                    ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
468 7
                    ->newLine();
469
470 7
                $reflectionClassCreated = true;
471
472 7
                break;
473
            }
474
        }
475
476
        /**
477
         * Iterate all properties and create internal scope reflection class instance if
478
         * at least one property in not public
479
         */
480 7
        if (!$reflectionClassCreated) {
481 7 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...
482 7
                if (!$methodMetadata->isPublic) {
483
                    $this->generator
484
                        ->comment('Create reflection class for injecting private/protected properties and methods')
485
                        ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
486
                        ->newLine();
487
488 7
                    break;
489
                }
490
            }
491
        }
492 7
    }
493
494
    /**
495
     * Build resolving property injection declaration.
496
     *
497
     * @param string $propertyName       Target property name
498
     * @param string $dependency         Dependency class name
499
     * @param string $containerVariable  Container declaration variable name
500
     * @param string $reflectionVariable Reflection class variable name
501
     * @param bool   $isPublic           Flag if property is public
502
     */
503 7
    protected function buildResolverPropertyDeclaration(
504
        string $propertyName,
505
        string $dependency,
506
        string $containerVariable,
507
        string $reflectionVariable,
508
        bool $isPublic
509
    )
510
    {
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...
511 7
        if ($isPublic) {
512 7
            $this->generator
513 7
                ->comment('Inject public dependency for $' . $propertyName)
514 7
                ->newLine($containerVariable . '->' . $propertyName . ' = ');
515 7
            $this->buildResolverArgument($dependency, 'text');
516 7
            $this->generator->text(';');
517
        } else {
518 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...
519 7
                ->comment('Inject private dependency for $' . $propertyName)
520 7
                ->newLine('$property = ' . $reflectionVariable . '->getProperty(\'' . $propertyName . '\');')
521 7
                ->newLine('$property->setAccessible(true);')
522 7
                ->newLine('$property->setValue(')
523 7
                ->increaseIndentation()
524 7
                ->newLine($containerVariable . ',');
525
526 7
            $this->buildResolverArgument($dependency);
527
528 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...
529 7
                ->decreaseIndentation()
530 7
                ->newLine(');')
531 7
                ->newLine('$property->setAccessible(false);')
532 7
                ->newLine();
533
        }
534 7
    }
535
536
    /**
537
     * Build resolving method injection declaration.
538
     *
539
     * @param array  $dependencies       Collection of method dependencies
540
     * @param string $methodName         Method name
541
     * @param string $containerVariable  Container declaration variable name
542
     * @param string $reflectionVariable Reflection class variable name
543
     * @param bool   $isPublic           Flag if method is public
544
     */
545 7
    protected function buildResolverMethodDeclaration(
546
        array $dependencies,
547
        string $methodName,
548
        string $containerVariable,
549
        string $reflectionVariable,
550
        bool $isPublic
551
    )
552
    {
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...
553
        // Get method arguments
554 7
        $argumentsCount = count($dependencies);
555
556 7
        $this->generator->comment('Invoke ' . $methodName . '() and pass dependencies(y)');
557
558 7
        if ($isPublic) {
559 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...
560
        } else {
561 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...
562 7
                ->newLine('$method = ' . $reflectionVariable . '->getMethod(\'' . $methodName . '\');')
563 7
                ->newLine('$method->setAccessible(true);')
564 7
                ->newLine('$method->invoke(')
565 7
                ->increaseIndentation()
566 7
                ->newLine($containerVariable . ',');
567
        }
568
569 7
        $i = 0;
570
        // Iterate method arguments
571 7
        foreach ($dependencies as $argument => $dependency) {
572
            // Add dependencies
573 7
            $this->buildResolverArgument($dependency);
574
575
            // Add comma if this is not last dependency
576 7
            if (++$i < $argumentsCount) {
577 7
                $this->generator->text(',');
578
            }
579
        }
580
581 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...
582 7
    }
583
}
584