Completed
Push — master ( 467d4c...6868f7 )
by Vitaly
03:13
created

Builder::buildResolverArgument()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 9
cts 10
cp 0.9
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 10
nc 5
nop 2
crap 5.025
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 . 'Instances';
34
35
    /** @var string[] Collection of available container scopes */
36
    protected $scopes = [
37
        self::SCOPE_CONTROLLER => [],
38
        self::SCOPE_SERVICES => []
39
    ];
40
41
    /** @var ClassMetadata[] Collection of classes metadata */
42
    protected $classesMetadata = [];
43
44
    /** @var array Collection of dependencies aliases */
45
    protected $classAliases = [];
46
47
    /**
48
     * @var Generator
49
     * @Injectable
50
     */
51
    protected $generator;
52
53
    /** @var string Resolver function name */
54
    protected $resolverFunction;
55
56
    /**
57
     * Container builder constructor.
58
     *
59
     * @param Generator       $generator     PHP code generator
60
     * @param ClassMetadata[] $classMetadata Collection of classes metadata for container
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
            ->multiComment(['@var array Collection of service instances'])
123 2
            ->defClassVar(self::DI_FUNCTION_SERVICES, 'public static', [])
124 2
            ->defClassFunction('__construct')
125 2
            ->newLine('$this->dependencies = ')->arrayValue($containerDependencies)->text(';')
126 2
            ->newLine('$this->aliases = ')->arrayValue($containerAliases)->text(';')
127 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...
128 2
            ->endClassFunction()
129 2
            ->defClassFunction('logic', 'protected', ['$dependency'], ['{@inheritdoc}'])
130 2
            ->newLine('return $this->' . $this->resolverFunction . '($dependency);')
131 2
            ->endClassFunction();
132
133 2
        foreach ($this->classesMetadata as $classMetadata) {
134 2
            $className = $classMetadata->className;
135 2
            $dependencyName = $classMetadata->name ?? $className;
136
137
            // Generate camel case getter method
138 2
            $camelMethodName = 'get' . str_replace(' ', '', ucwords(ucfirst(str_replace(['\\', '_'], ' ', $dependencyName))));
139
140 2
            $this->generator
141 2
                ->defClassFunction($camelMethodName, 'public', [], ['@return \\' . $className . ' Get ' . $dependencyName . ' instance'])
142 2
                ->newLine('return $this->' . $this->resolverFunction . '(\'' . $dependencyName . '\');')
143 2
                ->endClassFunction();
144
        }
145
146
        // Build di container function and add to container class and return class code
147 2
        $this->buildDependencyResolver($this->resolverFunction);
148
149 2
        return $this->generator
150 2
            ->endClass()
151 2
            ->flush();
152
    }
153
154
    /**
155
     * Build dependency resolving function.
156
     *
157
     * @param string $functionName Function name
158
     *
159
     * @throws \InvalidArgumentException
160
     */
161 2
    protected function buildDependencyResolver($functionName)
162
    {
163 2
        $inputVariable = '$aliasOrClassName';
164 2
        $this->generator
165 2
            ->defClassFunction($functionName, 'protected', [$inputVariable], ['Dependency resolving function'])
166
            //->defVar('static ' . self::DI_FUNCTION_SERVICES . ' = []')
167 2
            ->newLine();
168
169
        // Generate all container and delegate conditions
170 2
        $this->generateConditions($inputVariable, false);
171
172
        // Add method not found
173 2
        $this->generator->endIfCondition()->endFunction();
174 2
    }
175
176
    /**
177
     * Generate logic conditions and their implementation for container and its delegates.
178
     *
179
     * @param string     $inputVariable Input condition parameter variable name
180
     * @param bool|false $started       Flag if condition branching has been started
181
     */
182 2
    public function generateConditions($inputVariable = '$alias', $started = false)
183
    {
184
        // Iterate all container dependencies
185 2
        foreach ($this->classesMetadata as $classMetadata) {
186 2
            $className = $classMetadata->className;
187
            // Generate condition statement to define if this class is needed
188 2
            $conditionFunc = !$started ? 'defIfCondition' : 'defElseIfCondition';
189
190
            // Output condition branch
191 2
            $this->generator->$conditionFunc(
192 2
                $this->buildResolverCondition($inputVariable, $className, $classMetadata->name)
193
            );
194
195
            // Define if this class has service scope
196 2
            $isService = in_array($className, $this->scopes[self::SCOPE_SERVICES], true);
197
198
            /** @var MethodMetadata[] Gather only valid method for container */
199 2
            $classValidMethods = $this->getValidClassMethodsMetadata($classMetadata->methodsMetadata);
200
201
            /** @var PropertyMetadata[] Gather only valid property for container */
202 2
            $classValidProperties = $this->getValidClassPropertiesMetadata($classMetadata->propertiesMetadata);
203
204
            // Define class or service variable
205 2
            $staticContainerName = $isService
206 2
                ? 'static::' . self::DI_FUNCTION_SERVICES . '[\'' . $classMetadata->name . '\']'
207 2
                : '$temp';
208
209 2
            if ($isService) {
210
                // Check if dependency was instantiated
211 2
                $this->generator->defIfCondition('!array_key_exists(\'' . $className . '\', static::' . self::DI_FUNCTION_SERVICES . ')');
212
            }
213
214 2
            if (count($classValidMethods) || count($classValidProperties)) {
215 2
                $this->generator->newLine($staticContainerName . ' = ');
216 2
                $this->buildResolvingClassDeclaration($className);
217 2
                $this->buildConstructorDependencies($classMetadata->methodsMetadata);
218
219
                // Internal scope reflection variable
220 2
                $reflectionVariable = '$reflectionClass';
221
222 2
                $this->buildReflectionClass($className, $classValidProperties, $classValidMethods, $reflectionVariable);
223
224
                // Process class properties
225 2
                foreach ($classValidProperties as $property) {
226
                    // If such property has the dependency
227 2
                    if ($property->dependency) {
228
                        // Set value via refection
229 2
                        $this->buildResolverPropertyDeclaration(
230 2
                            $property->name,
231 2
                            $property->dependency,
232
                            $staticContainerName,
233
                            $reflectionVariable,
234 2
                            $property->isPublic
235
                        );
236
                    }
237
                }
238
239
                /** @var MethodMetadata $methodMetadata */
240 2
                foreach ($classValidMethods as $methodName => $methodMetadata) {
241 2
                    $this->buildResolverMethodDeclaration(
242 2
                        $methodMetadata->dependencies,
243
                        $methodName,
244
                        $staticContainerName,
245
                        $reflectionVariable,
246 2
                        $methodMetadata->isPublic
247
                    );
248
                }
249
250 2
                if ($isService) {
251 2
                    $this->generator->endIfCondition();
252
                }
253
254 2
                $this->generator->newLine()->newLine('return ' . $staticContainerName . ';');
255
            } else {
256 2
                $this->generator->newLine('return ');
257 2
                $this->buildResolvingClassDeclaration($className);
258 2
                $this->buildConstructorDependencies($classMetadata->methodsMetadata);
259
260 2
                if ($isService) {
261
                    $this->generator->endIfCondition()->newLine('return ' . $staticContainerName . ';');
262
                }
263
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
264
            }
265
266
            // Set flag that condition is started
267 2
            $started = true;
268
        }
269 2
    }
270
271
    /**
272
     * Build resolving function condition.
273
     *
274
     * @param string      $inputVariable Condition variable
275
     * @param string      $className
276
     * @param string|null $alias
277
     *
278
     * @return string Condition code
279
     */
280 2
    protected function buildResolverCondition(string $inputVariable, string $className, string $alias = null) : string
281
    {
282
        // Create condition branch
283 2
        $condition = $inputVariable . ' === \'' . $className . '\'';
284
285 2
        if ($alias !== null && $alias !== $className) {
286 2
            $condition .= '||' . $this->buildResolverCondition($inputVariable, $alias);
287
        }
288
289 2
        return $condition;
290
    }
291
292
    /**
293
     * Get valid class methods metadata.
294
     *
295
     * @param MethodMetadata[] $classMethodsMetadata All class methods metadata
296
     *
297
     * @return array Valid class methods metadata
298
     */
299 2
    protected function getValidClassMethodsMetadata(array $classMethodsMetadata)
300
    {
301
        /** @var MethodMetadata[] Gather only valid method for container */
302 2
        $classValidMethods = [];
303 2
        foreach ($classMethodsMetadata as $methodName => $methodMetadata) {
304
            // Skip constructor method and empty dependencies
305 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...
306 2
                $classValidMethods[$methodName] = $methodMetadata;
307
            }
308
        }
309
310 2
        return $classValidMethods;
311
    }
312
313
    /**
314
     * Get valid class properties metadata.
315
     *
316
     * @param PropertyMetadata[] $classPropertiesMetadata All class properties metadata
317
     *
318
     * @return array Valid class properties metadata
319
     */
320 2
    protected function getValidClassPropertiesMetadata(array $classPropertiesMetadata)
321
    {
322
        /** @var PropertyMetadata[] Gather only valid property for container */
323 2
        $classValidProperties = [];
324 2
        foreach ($classPropertiesMetadata as $propertyName => $propertyMetadata) {
325
            // Skip constructor method and empty dependencies
326 2
            if ($propertyMetadata->dependency) {
327 2
                $classValidProperties[$propertyName] = $propertyMetadata;
328
            }
329
        }
330
331 2
        return $classValidProperties;
332
    }
333
334
    /**
335
     * Build resolving function class block.
336
     *
337
     * @param string $className Class name for new instance creation
338
     */
339 2
    protected function buildResolvingClassDeclaration(string $className)
340
    {
341 2
        $this->generator->text('new \\' . ltrim($className, '\\') . '(');
342 2
    }
343
344
    /**
345
     * Build constructor arguments injection.
346
     *
347
     * @param MethodMetadata[] $methodsMetaData
348
     */
349 2
    protected function buildConstructorDependencies(array $methodsMetaData)
350
    {
351
        // Process constructor dependencies
352 2
        $argumentsCount = 0;
353 2
        if (array_key_exists('__construct', $methodsMetaData)) {
354 2
            $constructorArguments = $methodsMetaData['__construct']->dependencies ?? [];
355 2
            $argumentsCount = count($constructorArguments);
356 2
            $i = 0;
357
358
            // Add indentation to move declaration arguments
359 2
            $this->generator->tabs++;
360
361
            // Process constructor arguments
362 2
            foreach ($constructorArguments as $argument => $dependency) {
363 2
                $this->buildResolverArgument($dependency);
364
365
                // Add comma if this is not last dependency
366 2
                if (++$i < $argumentsCount) {
367 2
                    $this->generator->text(',');
368
                }
369
            }
370
371
            // Restore indentation
372 2
            $this->generator->tabs--;
373
        }
374
375
        // Close declaration block, multiline if we have dependencies
376 2
        $argumentsCount ? $this->generator->newLine(');') : $this->generator->text(');');
377 2
    }
378
379
    /**
380
     * Build resolving function dependency argument.
381
     *
382
     * @param mixed $argument Dependency argument
383
     */
384 2
    protected function buildResolverArgument($argument, $textFunction = 'newLine')
385
    {
386
        // This is a dependency which invokes resolving function
387 2
        if (is_string($argument)) {
388 2
            if (array_key_exists($argument, $this->classesMetadata)) {
389
                // Call container logic for this dependency
390 2
                $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
391 2
            } elseif (array_key_exists($argument, $this->classAliases)) {
392
                // Call container logic for this dependency
393
                $this->generator->$textFunction('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
394
            } else { // String variable
395 2
                $this->generator->$textFunction()->stringValue($argument);
396
            }
397 2
        } elseif (is_array($argument)) { // Dependency value is array
398 2
            $this->generator->$textFunction()->arrayValue($argument);
399
        }
400 2
    }
401
402
    /**
403
     * Generate reflection class for private/protected methods or properties
404
     * in current scope.
405
     *
406
     * @param string             $className          Reflection class source class name
407
     * @param PropertyMetadata[] $propertiesMetadata Properties metadata
408
     * @param MethodMetadata[]   $methodsMetadata    Methods metadata
409
     * @param string             $reflectionVariable Reflection class variable name
410
     */
411 2
    protected function buildReflectionClass(string $className, array $propertiesMetadata, array $methodsMetadata, string $reflectionVariable)
412
    {
413
        /**
414
         * Iterate all properties and create internal scope reflection class instance if
415
         * at least one property in not public
416
         */
417 2 View Code Duplication
        foreach ($propertiesMetadata as $propertyMetadata) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
418 2
            if (!$propertyMetadata->isPublic) {
419 2
                $this->generator
420 2
                    ->comment('Create reflection class for injecting private/protected properties and methods')
421 2
                    ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
422 2
                    ->newLine();
423
424 2
                return true;
425
            }
426
        }
427
428
        /**
429
         * Iterate all properties and create internal scope reflection class instance if
430
         * at least one property in not public
431
         */
432 2 View Code Duplication
        foreach ($methodsMetadata as $methodMetadata) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
433 2
            if (!$methodMetadata->isPublic) {
434 2
                $this->generator
435 2
                    ->comment('Create reflection class for injecting private/protected properties and methods')
436 2
                    ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
437 2
                    ->newLine();
438
439 2
                return true;
440
            }
441
        }
442
443 2
        return false;
444
    }
445
446
    /**
447
     * Build resolving property injection declaration.
448
     *
449
     * @param string $propertyName       Target property name
450
     * @param string $dependency         Dependency class name
451
     * @param string $containerVariable  Container declaration variable name
452
     * @param string $reflectionVariable Reflection class variable name
453
     * @param bool   $isPublic           Flag if property is public
454
     */
455 2
    protected function buildResolverPropertyDeclaration(
456
        string $propertyName,
457
        string $dependency,
458
        string $containerVariable,
459
        string $reflectionVariable,
460
        bool $isPublic
461
    )
462
    {
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...
463 2
        if ($isPublic) {
464 2
            $this->generator
465 2
                ->comment('Inject public dependency for $' . $propertyName)
466 2
                ->newLine($containerVariable . '->' . $propertyName . ' = ');
467 2
            $this->buildResolverArgument($dependency, 'text');
468 2
            $this->generator->text(';');
469
        } else {
470 2
            $this->generator
471 2
                ->comment('Inject private dependency for $' . $propertyName)
472 2
                ->newLine('$property = ' . $reflectionVariable . '->getProperty(\'' . $propertyName . '\');')
473 2
                ->newLine('$property->setAccessible(true);')
474 2
                ->newLine('$property->setValue(')
475 2
                ->increaseIndentation()
476 2
                ->newLine($containerVariable . ',');
477
478 2
            $this->buildResolverArgument($dependency);
479
480 2
            $this->generator
481 2
                ->decreaseIndentation()
482 2
                ->newLine(');')
483 2
                ->newLine('$property->setAccessible(false);')
484 2
                ->newLine();
485
        }
486 2
    }
487
488
    /**
489
     * Build resolving method injection declaration.
490
     *
491
     * @param array  $dependencies       Collection of method dependencies
492
     * @param string $methodName         Method name
493
     * @param string $containerVariable  Container declaration variable name
494
     * @param string $reflectionVariable Reflection class variable name
495
     * @param bool   $isPublic           Flag if method is public
496
     */
497 2
    protected function buildResolverMethodDeclaration(
498
        array $dependencies,
499
        string $methodName,
500
        string $containerVariable,
501
        string $reflectionVariable,
502
        bool $isPublic
503
    )
504
    {
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...
505
        // Get method arguments
506 2
        $argumentsCount = count($dependencies);
507
508 2
        $this->generator->comment('Invoke ' . $methodName . '() and pass dependencies(y)');
509
510 2
        if ($isPublic) {
511 2
            $this->generator->newLine($containerVariable . '->' . $methodName . '(')->increaseIndentation();
512
        } else {
513 2
            $this->generator
514 2
                ->newLine('$method = ' . $reflectionVariable . '->getMethod(\'' . $methodName . '\');')
515 2
                ->newLine('$method->setAccessible(true);')
516 2
                ->newLine('$method->invoke(')
517 2
                ->increaseIndentation()
518 2
                ->newLine($containerVariable . ',');
519
        }
520
521 2
        $i = 0;
522
        // Iterate method arguments
523 2
        foreach ($dependencies as $argument => $dependency) {
524
            // Add dependencies
525 2
            $this->buildResolverArgument($dependency);
526
527
            // Add comma if this is not last dependency
528 2
            if (++$i < $argumentsCount) {
529 2
                $this->generator->text(',');
530
            }
531
        }
532
533 2
        $this->generator->decreaseIndentation()->newLine(');');
534 2
    }
535
}
536