Completed
Push — master ( 01582c...42d944 )
by Vitaly
02:30
created

Builder::processClassMetadata()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 5
nc 3
nop 1
crap 3
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 2
    public function __construct(Generator $generator, array $classMetadata)
63
    {
64 2
        $this->generator = $generator;
65 2
        $this->classesMetadata = $classMetadata;
66
67 2
        $this->processClassMetadata($this->classesMetadata);
68 2
    }
69
70
    /**
71
     * Read class metadata and fill internal collections.
72
     *
73
     * @param ClassMetadata[] $classesMetadata
74
     */
75 2
    public function processClassMetadata(array $classesMetadata)
76
    {
77
        // Read all classes in given file
78 2
        foreach ($classesMetadata as $classMetadata) {
79
            // Store by metadata name as alias
80 2
            $this->classAliases[$classMetadata->name] = $classMetadata->className;
81
82
            // Store class in defined scopes
83 2
            foreach ($classMetadata->scopes as $scope) {
84 2
                $this->scopes[$scope][] = $classMetadata->className;
85
            }
86
        }
87 2
    }
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 2
    public function build($containerClass = 'Container', $namespace = '')
99
    {
100
        // Build dependency injection container function name
101 2
        $this->resolverFunction = uniqid(self::DI_FUNCTION_PREFIX);
102
103 2
        $containerDependencies = [];
104 2
        $containerAliases = [];
105 2
        foreach ($this->classesMetadata as $classMetadata) {
106 2
            $className = $classMetadata->className;
107 2
            if ($classMetadata->alias !== null) {
108
                $containerAliases[$className] = $classMetadata->alias;
109
            }
110
            // Store inner dependencies
111 2
            if (array_key_exists('__construct', $classMetadata->methodsMetadata)) {
112 2
                $containerDependencies[$className] = array_values($classMetadata->methodsMetadata['__construct']->dependencies ?? []);
113
            }
114
        }
115
116 2
        $this->generator
117 2
            ->text('<?php declare(strict_types = 1);')
118 2
            ->newLine()
119 2
            ->defNamespace($namespace)
120 2
            ->multiComment(['Application container'])
121 2
            ->defClass($containerClass, '\\' . Container::class)
122 2
            ->defClassFunction('__construct')
123 2
            ->newLine('$this->dependencies = ')->arrayValue($containerDependencies)->text(';')
124 2
            ->newLine('$this->aliases = ')->arrayValue($containerAliases)->text(';')
125 2
            ->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 2
            ->endClassFunction()
127 2
            ->defClassFunction('logic', 'protected', ['$dependency'], ['{@inheritdoc}'])
128 2
            ->newLine('return $this->' . $this->resolverFunction . '($dependency);')
129 2
            ->endClassFunction();
130
131 2
        foreach ($this->classesMetadata as $classMetadata) {
132 2
            $className = $classMetadata->className;
133 2
            $dependencyName = $classMetadata->name ?? $className;
134
135
            // Generate camel case getter method
136 2
            $camelMethodName = 'get' . str_replace(' ', '', ucwords(ucfirst(str_replace(['\\', '_'], ' ', $dependencyName))));
137
138 2
            $this->generator
139 2
                ->defClassFunction($camelMethodName, 'public', [], ['@return \\' . $className . ' Get ' . $dependencyName . ' instance'])
140 2
                ->newLine('return $this->' . $this->resolverFunction . '(\'' . $dependencyName . '\');')
141 2
                ->endClassFunction();
142
        }
143
144
        // Build di container function and add to container class and return class code
145 2
        $this->buildDependencyResolver($this->resolverFunction);
146
147 2
        return $this->generator
148 2
            ->endClass()
149 2
            ->flush();
150
    }
151
152
    /**
153
     * Build dependency resolving function.
154
     *
155
     * @param string $functionName Function name
156
     *
157
     * @throws \InvalidArgumentException
158
     */
159 2
    protected function buildDependencyResolver($functionName)
160
    {
161 2
        $inputVariable = '$aliasOrClassName';
162 2
        $this->generator
163 2
            ->defClassFunction($functionName, 'protected', [$inputVariable], ['Dependency resolving function'])
164 2
            ->defVar('static ' . self::DI_FUNCTION_SERVICES . ' = []')
165 2
            ->newLine();
166
167
        // Generate all container and delegate conditions
168 2
        $this->generateConditions($inputVariable, false);
169
170
        // Add method not found
171 2
        $this->generator->endIfCondition()->endFunction();
172 2
    }
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 2
    public function generateConditions($inputVariable = '$alias', $started = false)
181
    {
182
        // Iterate all container dependencies
183 2
        foreach ($this->classesMetadata as $classMetadata) {
184 2
            $className = $classMetadata->className;
185
            // Generate condition statement to define if this class is needed
186 2
            $conditionFunc = !$started ? 'defIfCondition' : 'defElseIfCondition';
187
188
            // Output condition branch
189 2
            $this->generator->$conditionFunc(
190 2
                $this->buildResolverCondition($inputVariable, $className, $classMetadata->name)
191
            );
192
193
            // Define if this class has service scope
194 2
            $isService = in_array($className, $this->scopes[self::SCOPE_SERVICES], true);
195
196
            /** @var MethodMetadata[] Gather only valid method for container */
197 2
            $classValidMethods = $this->getValidClassMethodsMetadata($classMetadata->methodsMetadata);
198
199
            /** @var PropertyMetadata[] Gather only valid property for container */
200 2
            $classValidProperties = $this->getValidClassPropertiesMetadata($classMetadata->propertiesMetadata);
201
202
            // Define class or service variable
203 2
            $staticContainerName = $isService
204 1
                ? self::DI_FUNCTION_SERVICES . '[\'' . $classMetadata->name . '\']'
205 2
                : '$temp';
206
207 2
            if ($isService) {
208
                // Check if dependency was instantiated
209 1
                $this->generator->defIfCondition('!array_key_exists(\'' . $className . '\', ' . self::DI_FUNCTION_SERVICES . ')');
210
            }
211
212 2
            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
                    $this->generator->endIfCondition();
250
                }
251
252 1
                $this->generator->newLine()->newLine('return ' . $staticContainerName . ';');
253
            } else {
254 2
                $this->generator->newLine('return ');
255 2
                $this->buildResolvingClassDeclaration($className);
256 2
                $this->buildConstructorDependencies($classMetadata->methodsMetadata);
257
258 2
                if ($isService) {
259 1
                    $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 2
            $started = true;
266
        }
267 2
    }
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 2
    protected function buildResolverCondition(string $inputVariable, string $className, string $alias = null) : string
279
    {
280
        // Create condition branch
281 2
        $condition = $inputVariable . ' === \'' . $className . '\'';
282
283 2
        if ($alias !== null && $alias !== $className) {
284 2
            $condition .= '||' . $this->buildResolverCondition($inputVariable, $alias);
285
        }
286
287 2
        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 2
    protected function getValidClassMethodsMetadata(array $classMethodsMetadata)
298
    {
299
        /** @var MethodMetadata[] Gather only valid method for container */
300 2
        $classValidMethods = [];
301 2
        foreach ($classMethodsMetadata as $methodName => $methodMetadata) {
302
            // Skip constructor method and empty dependencies
303 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...
304 2
                $classValidMethods[$methodName] = $methodMetadata;
305
            }
306
        }
307
308 2
        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 2
    protected function getValidClassPropertiesMetadata(array $classPropertiesMetadata)
319
    {
320
        /** @var PropertyMetadata[] Gather only valid property for container */
321 2
        $classValidProperties = [];
322 2
        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 2
        return $classValidProperties;
330
    }
331
332
    /**
333
     * Build resolving function class block.
334
     *
335
     * @param string $className Class name for new instance creation
336
     */
337 2
    protected function buildResolvingClassDeclaration(string $className)
338
    {
339 2
        $this->generator->text('new \\' . ltrim($className, '\\') . '(');
340 2
    }
341
342
    /**
343
     * Build constructor arguments injection.
344
     *
345
     * @param MethodMetadata[] $methodsMetaData
346
     */
347 2
    protected function buildConstructorDependencies(array $methodsMetaData)
348
    {
349
        // Process constructor dependencies
350 2
        $argumentsCount = 0;
351 2
        if (array_key_exists('__construct', $methodsMetaData)) {
352 2
            $constructorArguments = $methodsMetaData['__construct']->dependencies ?? [];
353 2
            $argumentsCount = count($constructorArguments);
354 2
            $i = 0;
355
356
            // Add indentation to move declaration arguments
357 2
            $this->generator->tabs++;
358
359
            // Process constructor arguments
360 2
            foreach ($constructorArguments as $argument => $dependency) {
361 2
                $this->buildResolverArgument($dependency);
362
363
                // Add comma if this is not last dependency
364 2
                if (++$i < $argumentsCount) {
365 2
                    $this->generator->text(',');
366
                }
367
            }
368
369
            // Restore indentation
370 2
            $this->generator->tabs--;
371
        }
372
373
        // Close declaration block, multiline if we have dependencies
374 2
        $argumentsCount ? $this->generator->newLine(');') : $this->generator->text(');');
375 2
    }
376
377
    /**
378
     * Build resolving function dependency argument.
379
     *
380
     * @param mixed $argument Dependency argument
381
     */
382 2
    protected function buildResolverArgument($argument, $textFunction = 'newLine')
383
    {
384
        // This is a dependency which invokes resolving function
385 2
        if (array_key_exists($argument, $this->classesMetadata)) {
386
            // Call container logic for this dependency
387 1
            $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
388 1
        } elseif (array_key_exists($argument, $this->classAliases)) {
389
            // Call container logic for this dependency
390 1
            $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
391 1
        } elseif (is_string($argument)) { // String variable
392 1
            $this->generator->$textFunction()->stringValue($argument);
393
        } elseif (is_array($argument)) { // Dependency value is array
394
            $this->generator->$textFunction()->arrayValue($argument);
395
        }
396 2
    }
397
398
    /**
399
     * Generate reflection class for private/protected methods or properties
400
     * in current scope.
401
     *
402
     * @param string             $className          Reflection class source class name
403
     * @param PropertyMetadata[] $propertiesMetadata Properties metadata
404
     * @param MethodMetadata[]   $methodsMetadata    Methods metadata
405
     * @param string             $reflectionVariable Reflection class variable name
406
     */
407 1
    protected function buildReflectionClass(string $className, array $propertiesMetadata, array $methodsMetadata, string $reflectionVariable)
408
    {
409
        /** @var bool $reflectionClassCreated Flag showing that reflection class already created in current scope */
410 1
        $reflectionClassCreated = false;
411
412
        /**
413
         * Iterate all properties and create internal scope reflection class instance if
414
         * at least one property in not public
415
         */
416 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...
417 1
            if (!$propertyMetadata->isPublic) {
418 1
                $this->generator
419 1
                    ->comment('Create reflection class for injecting private/protected properties and methods')
420 1
                    ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
421 1
                    ->newLine();
422
423 1
                $reflectionClassCreated = true;
424
425 1
                break;
426
            }
427
        }
428
429
        /**
430
         * Iterate all properties and create internal scope reflection class instance if
431
         * at least one property in not public
432
         */
433 1
        if (!$reflectionClassCreated) {
434 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...
435 1
                if (!$methodMetadata->isPublic) {
436
                    $this->generator
437
                        ->comment('Create reflection class for injecting private/protected properties and methods')
438
                        ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
439
                        ->newLine();
440
441 1
                    break;
442
                }
443
            }
444
        }
445 1
    }
446
447
    /**
448
     * Build resolving property injection declaration.
449
     *
450
     * @param string $propertyName       Target property name
451
     * @param string $dependency         Dependency class name
452
     * @param string $containerVariable  Container declaration variable name
453
     * @param string $reflectionVariable Reflection class variable name
454
     * @param bool   $isPublic           Flag if property is public
455
     */
456 1
    protected function buildResolverPropertyDeclaration(
457
        string $propertyName,
458
        string $dependency,
459
        string $containerVariable,
460
        string $reflectionVariable,
461
        bool $isPublic
462
    )
463
    {
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...
464 1
        if ($isPublic) {
465
            $this->generator
466
                ->comment('Inject public dependency for $' . $propertyName)
467
                ->newLine($containerVariable . '->' . $propertyName . ' = ');
468
            $this->buildResolverArgument($dependency, 'text');
469
            $this->generator->text(';');
470
        } else {
471 1
            $this->generator
472 1
                ->comment('Inject private dependency for $' . $propertyName)
473 1
                ->newLine('$property = ' . $reflectionVariable . '->getProperty(\'' . $propertyName . '\');')
474 1
                ->newLine('$property->setAccessible(true);')
475 1
                ->newLine('$property->setValue(')
476 1
                ->increaseIndentation()
477 1
                ->newLine($containerVariable . ',');
478
479 1
            $this->buildResolverArgument($dependency);
480
481 1
            $this->generator
482 1
                ->decreaseIndentation()
483 1
                ->newLine(');')
484 1
                ->newLine('$property->setAccessible(false);')
485 1
                ->newLine();
486
        }
487 1
    }
488
489
    /**
490
     * Build resolving method injection declaration.
491
     *
492
     * @param array  $dependencies       Collection of method dependencies
493
     * @param string $methodName         Method name
494
     * @param string $containerVariable  Container declaration variable name
495
     * @param string $reflectionVariable Reflection class variable name
496
     * @param bool   $isPublic           Flag if method is public
497
     */
498 1
    protected function buildResolverMethodDeclaration(
499
        array $dependencies,
500
        string $methodName,
501
        string $containerVariable,
502
        string $reflectionVariable,
503
        bool $isPublic
504
    )
505
    {
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...
506
        // Get method arguments
507 1
        $argumentsCount = count($dependencies);
508
509 1
        $this->generator->comment('Invoke ' . $methodName . '() and pass dependencies(y)');
510
511 1
        if ($isPublic) {
512 1
            $this->generator->newLine($containerVariable . '->' . $methodName . '(')->increaseIndentation();
513
        } else {
514
            $this->generator
515
                ->newLine('$method = ' . $reflectionVariable . '->getMethod(\'' . $methodName . '\');')
516
                ->newLine('$method->setAccessible(true);')
517
                ->newLine('$method->invoke(')
518
                ->increaseIndentation()
519
                ->newLine($containerVariable . ',');
520
        }
521
522 1
        $i = 0;
523
        // Iterate method arguments
524 1
        foreach ($dependencies as $argument => $dependency) {
525
            // Add dependencies
526 1
            $this->buildResolverArgument($dependency);
527
528
            // Add comma if this is not last dependency
529 1
            if (++$i < $argumentsCount) {
530 1
                $this->generator->text(',');
531
            }
532
        }
533
534 1
        $this->generator->decreaseIndentation()->newLine(');');
535 1
    }
536
}
537