Completed
Push — master ( 4bcb6e...27d771 )
by Vitaly
03:27
created

Builder::getValidClassMethodsMetadata()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

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