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

ContainerBuilder::getDefinedClasses()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

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