Completed
Push — master ( 05d0c3...5bb4f6 )
by
unknown
03:13
created

Builder::buildResolverArgument()   C

Complexity

Conditions 9
Paths 8

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 10.2655

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 12
cts 16
cp 0.75
rs 5.3563
c 0
b 0
f 0
cc 9
eloc 16
nc 8
nop 2
crap 10.2655
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\ContainerInterface;
11
use samsonframework\container\metadata\ClassMetadata;
12
use samsonframework\container\metadata\MethodMetadata;
13
use samsonframework\container\metadata\PropertyMetadata;
14
use samsonframework\di\Container;
15
use samsonphp\generator\Generator;
16
17
/**
18
 * Container builder.
19
 *
20
 * @author Vitaly Egorov <[email protected]>
21
 */
22
class Builder implements ContainerBuilderInterface
23
{
24
    /** Controller classes scope name */
25
    const SCOPE_CONTROLLER = 'controllers';
26
27
    /** Service classes scope name */
28
    const SCOPE_SERVICES = 'service';
29
30
    /** Generated resolving function name prefix */
31
    const DI_FUNCTION_PREFIX = 'container';
32
33
    /** Generated resolving function service static collection name */
34
    const DI_FUNCTION_SERVICES = self::SCOPE_SERVICES . 'Instances';
35
36
    /** @var string[] Collection of available container scopes */
37
    protected $scopes = [
38
        self::SCOPE_CONTROLLER => [],
39
        self::SCOPE_SERVICES => []
40
    ];
41
42
    /** @var ClassMetadata[] Collection of classes metadata */
43
    protected $classesMetadata = [];
44
45
    /** @var array Collection of dependencies aliases */
46
    protected $classAliases = [];
47
48
    /** @var  ContainerInterface */
49
    protected $parentContainer;
50
51
    /**
52
     * @var Generator
53
     * @Injectable
54
     */
55
    protected $generator;
56
57
    /** @var string Resolver function name */
58
    protected $resolverFunction;
59
60
    /**
61
     * Container builder constructor.
62
     *
63
     * @param Generator       $generator     PHP code generator
64
     * @param ClassMetadata[] $classMetadata Collection of classes metadata for container
0 ignored issues
show
Bug introduced by
There is no parameter named $classMetadata. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
65
     */
66 2
    public function __construct(Generator $generator)
67
    {
68 2
        $this->generator = $generator;
69 2
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 2
    public function build(array $classesMetadata, $containerClass = 'Container', $namespace = '', ContainerInterface $parentContainer = null)
75
    {
76 2
        $this->classesMetadata = $this->processClassMetadata($classesMetadata);
77 2
        $this->parentContainer = $parentContainer;
78
79
        // Build dependency injection container function name
80 2
        $this->resolverFunction = uniqid(self::DI_FUNCTION_PREFIX);
81
82 2
        $containerDependencies = [];
83 2
        foreach ($classesMetadata as $classMetadata) {
84 2
            $className = $classMetadata->className;
85
            // Store inner dependencies
86 2
            if (array_key_exists('__construct', $classMetadata->methodsMetadata)) {
87 2
                $containerDependencies[$className] = array_values($classMetadata->methodsMetadata['__construct']->dependencies ?? []);
88
            }
89
        }
90
91 2
        $this->generator
92 2
            ->text('<?php declare(strict_types = 1);')
93 2
            ->newLine()
94 2
            ->defNamespace($namespace)
95 2
            ->multiComment(['Application container'])
96 2
            ->defClass($containerClass, '\\' . Container::class)
97 2
            ->multiComment(['@var array Collection of service instances'])
98 2
            ->defClassFunction('__construct', 'public', [], ['Container constructor'])
99 2
            ->newLine('$this->dependencies = ')->arrayValue($containerDependencies)->text(';')
100 2
            ->newLine('$this->aliases = ')->arrayValue($this->classAliases)->text(';')
101 2
            ->newLine('$this->scopes = ')->arrayValue($this->scopes)->text(';')
102 2
            ->newLine('$this->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...
103 2
            ->endClassFunction()
104 2
            ->defClassFunction('logic', 'protected', ['$dependency'], ['{@inheritdoc}'])
105 2
            ->newLine('return $this->' . $this->resolverFunction . '($dependency);')
106 2
            ->endClassFunction();
107
108 2
        foreach ($classesMetadata as $classMetadata) {
109 2
            $className = $classMetadata->className;
110 2
            $dependencyName = $classMetadata->name ?? $className;
111
112
            // Generate camel case getter method
113 2
            $camelMethodName = 'get' . str_replace(' ', '', ucwords(ucfirst(str_replace(['\\', '_'], ' ', $dependencyName))));
114
115 2
            $this->generator
116 2
                ->defClassFunction($camelMethodName, 'public', [], ['@return ' . '\\'.ltrim($className, '\\') . ' Get ' . $dependencyName . ' instance'])
117 2
                ->newLine('return $this->' . $this->resolverFunction . '(\'' . $dependencyName . '\');')
118 2
                ->endClassFunction();
119
        }
120
121
        // Build di container function and add to container class and return class code
122 2
        $this->buildDependencyResolver($this->resolverFunction);
123
124 2
        return $this->generator
125 2
            ->endClass()
126 2
            ->flush();
127
    }
128
129
    /**
130
     * Read class metadata and fill internal collections.
131
     *
132
     * @param ClassMetadata[] $classesMetadata
133
     * @return ClassMetadata[] Processed class metadata
134
     */
135 2
    public function processClassMetadata(array $classesMetadata) : array
136
    {
137 2
        $processedClassesMetadata = [];
138
139
        // Read all classes in given file
140 2
        foreach ($classesMetadata as $classMetadata) {
141
            // Store by metadata name as alias
142 2
            $this->classAliases[$classMetadata->className] = $classMetadata->name;
143
144
            // Store class in defined scopes
145 2
            foreach ($classMetadata->scopes as $scope) {
146 2
                $this->scopes[$scope][$classMetadata->name] = $classMetadata->className;
147
            }
148 2
            $processedClassesMetadata[$classMetadata->name] = $classMetadata;
149
        }
150
151 2
        return $processedClassesMetadata;
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
                ? '$this->' . 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(\'' . $classMetadata->name . '\', $this->' . 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
                if ($isService) {
257
                    $this->generator->newLine($staticContainerName.' = ');
258
                    $this->buildResolvingClassDeclaration($className);
259
                    $this->buildConstructorDependencies($classMetadata->methodsMetadata);
260
                    $this->generator->endIfCondition()->newLine('return ' . $staticContainerName . ';');
261
                } else {
262 2
                    $this->generator->newLine('return ');
263 2
                    $this->buildResolvingClassDeclaration($className);
264 2
                    $this->buildConstructorDependencies($classMetadata->methodsMetadata);
265
                }
266
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
267
            }
268
269
            // Set flag that condition is started
270 2
            $started = true;
271
        }
272 2
    }
273
274
    /**
275
     * Build resolving function condition.
276
     *
277
     * @param string      $inputVariable Condition variable
278
     * @param string      $className
279
     * @param string|null $alias
280
     *
281
     * @return string Condition code
282
     */
283 2
    protected function buildResolverCondition(string $inputVariable, string $className, string $alias = null) : string
284
    {
285
        // Create condition branch
286 2
        $condition = $inputVariable . ' === \'' . $className . '\'';
287
288 2
        if ($alias !== null && $alias !== $className) {
289 2
            $condition .= '||' . $this->buildResolverCondition($inputVariable, $alias);
290
        }
291
292 2
        return $condition;
293
    }
294
295
    /**
296
     * Get valid class methods metadata.
297
     *
298
     * @param MethodMetadata[] $classMethodsMetadata All class methods metadata
299
     *
300
     * @return array Valid class methods metadata
301
     */
302 2
    protected function getValidClassMethodsMetadata(array $classMethodsMetadata)
303
    {
304
        /** @var MethodMetadata[] Gather only valid method for container */
305 2
        $classValidMethods = [];
306 2
        foreach ($classMethodsMetadata as $methodName => $methodMetadata) {
307
            // Skip constructor method and empty dependencies
308 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...
309 2
                $classValidMethods[$methodName] = $methodMetadata;
310
            }
311
        }
312
313 2
        return $classValidMethods;
314
    }
315
316
    /**
317
     * Get valid class properties metadata.
318
     *
319
     * @param PropertyMetadata[] $classPropertiesMetadata All class properties metadata
320
     *
321
     * @return array Valid class properties metadata
322
     */
323 2
    protected function getValidClassPropertiesMetadata(array $classPropertiesMetadata)
324
    {
325
        /** @var PropertyMetadata[] Gather only valid property for container */
326 2
        $classValidProperties = [];
327 2
        foreach ($classPropertiesMetadata as $propertyName => $propertyMetadata) {
328
            // Skip constructor method and empty dependencies
329 2
            if ($propertyMetadata->dependency) {
330 2
                $classValidProperties[$propertyName] = $propertyMetadata;
331
            }
332
        }
333
334 2
        return $classValidProperties;
335
    }
336
337
    /**
338
     * Build resolving function class block.
339
     *
340
     * @param string $className Class name for new instance creation
341
     */
342 2
    protected function buildResolvingClassDeclaration(string $className)
343
    {
344 2
        $this->generator->text('new \\' . ltrim($className, '\\') . '(');
345 2
    }
346
347
    /**
348
     * Build constructor arguments injection.
349
     *
350
     * @param MethodMetadata[] $methodsMetaData
351
     */
352 2
    protected function buildConstructorDependencies(array $methodsMetaData)
353
    {
354
        // Process constructor dependencies
355 2
        $argumentsCount = 0;
356 2
        if (array_key_exists('__construct', $methodsMetaData)) {
357 2
            $constructorArguments = $methodsMetaData['__construct']->dependencies ?? [];
358 2
            $argumentsCount = count($constructorArguments);
359 2
            $i = 0;
360
361
            // Add indentation to move declaration arguments
362 2
            $this->generator->tabs++;
363
364
            // Process constructor arguments
365 2
            foreach ($constructorArguments as $argument => $parameterMetadata) {
366 2
                $this->buildResolverArgument($parameterMetadata);
367
368
                // Add comma if this is not last dependency
369 2
                if (++$i < $argumentsCount) {
370 2
                    $this->generator->text(',');
371
                }
372
            }
373
374
            // Restore indentation
375 2
            $this->generator->tabs--;
376
        }
377
378
        // Close declaration block, multiline if we have dependencies
379 2
        $argumentsCount ? $this->generator->newLine(');') : $this->generator->text(');');
380 2
    }
381
382
    /**
383
     * Build resolving function dependency argument.
384
     *
385
     * @param mixed $argument Dependency argument
386
     *
387
     * @throws \InvalidArgumentException On invalid argument type
388
     */
389 2
    protected function buildResolverArgument($argument, $textFunction = 'newLine')
390
    {
391
        // This is a dependency which invokes resolving function
392 2
        if (is_string($argument)) {
393 2
            if ($this->parentContainer !== null && $this->parentContainer->has($argument)) {
394
                // Call container logic for this dependency
395
                $this->generator->$textFunction('$this->get(\'' . $argument . '\')');
396 2
            } elseif (array_key_exists($argument, $this->classesMetadata)) {
397
                // Call container logic for this dependency
398
                $this->generator->$textFunction('$this->get(\'' . $argument . '\')');
399 2
            } elseif (array_key_exists($argument, $this->classAliases)) {
400
                // Call container logic for this dependency
401 2
                $this->generator->$textFunction('$this->get(\'' . $this->classAliases[$argument] . '\')');
402 2
            } elseif (class_exists($argument)) { // If this argument is existing class
403
                throw new \InvalidArgumentException($argument.' class metadata is not defined');
404 2
            } elseif (interface_exists($argument)) { // If this argument is existing interface
405
                throw new \InvalidArgumentException($argument.' - interface dependency not resolvable');
406
            } else { // String variable
407 2
                $this->generator->$textFunction()->stringValue($argument);
408
            }
409 2
        } elseif (is_array($argument)) { // Dependency value is array
410 2
            $this->generator->$textFunction()->arrayValue($argument);
411
        }
412 2
    }
413
414
    /**
415
     * Generate reflection class for private/protected methods or properties
416
     * in current scope.
417
     *
418
     * @param string             $className          Reflection class source class name
419
     * @param PropertyMetadata[] $propertiesMetadata Properties metadata
420
     * @param MethodMetadata[]   $methodsMetadata    Methods metadata
421
     * @param string             $reflectionVariable Reflection class variable name
422
     */
423 2
    protected function buildReflectionClass(string $className, array $propertiesMetadata, array $methodsMetadata, string $reflectionVariable)
424
    {
425
        /**
426
         * Iterate all properties and create internal scope reflection class instance if
427
         * at least one property in not public
428
         */
429 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...
430 2
            if (!$propertyMetadata->isPublic) {
431 2
                $this->generator
432 2
                    ->comment('Create reflection class for injecting private/protected properties and methods')
433 2
                    ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
434 2
                    ->newLine();
435
436 2
                return true;
437
            }
438
        }
439
440
        /**
441
         * Iterate all properties and create internal scope reflection class instance if
442
         * at least one property in not public
443
         */
444 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...
445
            if (!$methodMetadata->isPublic) {
446
                $this->generator
447
                    ->comment('Create reflection class for injecting private/protected properties and methods')
448
                    ->newLine($reflectionVariable . ' = new \ReflectionClass(\'' . $className . '\');')
449
                    ->newLine();
450
451
                return true;
452
            }
453
        }
454
455 2
        return false;
456
    }
457
458
    /**
459
     * Build resolving property injection declaration.
460
     *
461
     * @param string $propertyName       Target property name
462
     * @param string $dependency         Dependency class name
463
     * @param string $containerVariable  Container declaration variable name
464
     * @param string $reflectionVariable Reflection class variable name
465
     * @param bool   $isPublic           Flag if property is public
466
     */
467 2
    protected function buildResolverPropertyDeclaration(
468
        string $propertyName,
469
        string $dependency,
470
        string $containerVariable,
471
        string $reflectionVariable,
472
        bool $isPublic
473
    )
474
    {
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...
475 2
        if ($isPublic) {
476 2
            $this->generator
477 2
                ->comment('Inject public dependency for $' . $propertyName)
478 2
                ->newLine($containerVariable . '->' . $propertyName . ' = ');
479 2
            $this->buildResolverArgument($dependency, 'text');
480 2
            $this->generator->text(';');
481
        } else {
482 2
            $this->generator
483 2
                ->comment('Inject private dependency for $' . $propertyName)
484 2
                ->newLine('$property = ' . $reflectionVariable . '->getProperty(\'' . $propertyName . '\');')
485 2
                ->newLine('$property->setAccessible(true);')
486 2
                ->newLine('$property->setValue(')
487 2
                ->increaseIndentation()
488 2
                ->newLine($containerVariable . ',');
489
490 2
            $this->buildResolverArgument($dependency);
491
492 2
            $this->generator
493 2
                ->decreaseIndentation()
494 2
                ->newLine(');')
495 2
                ->newLine('$property->setAccessible(false);')
496 2
                ->newLine();
497
        }
498 2
    }
499
500
    /**
501
     * Build resolving method injection declaration.
502
     *
503
     * @param array  $dependencies       Collection of method dependencies
504
     * @param string $methodName         Method name
505
     * @param string $containerVariable  Container declaration variable name
506
     * @param string $reflectionVariable Reflection class variable name
507
     * @param bool   $isPublic           Flag if method is public
508
     */
509 2
    protected function buildResolverMethodDeclaration(
510
        array $dependencies,
511
        string $methodName,
512
        string $containerVariable,
513
        string $reflectionVariable,
514
        bool $isPublic
515
    )
516
    {
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...
517
        // Get method arguments
518 2
        $argumentsCount = count($dependencies);
519
520 2
        $this->generator->comment('Invoke ' . $methodName . '() and pass dependencies(y)');
521
522 2
        if ($isPublic) {
523 2
            $this->generator->newLine($containerVariable . '->' . $methodName . '(')->increaseIndentation();
524
        } else {
525 2
            $this->generator
526 2
                ->newLine('$method = ' . $reflectionVariable . '->getMethod(\'' . $methodName . '\');')
527 2
                ->newLine('$method->setAccessible(true);')
528 2
                ->newLine('$method->invoke(')
529 2
                ->increaseIndentation()
530 2
                ->newLine($containerVariable . ',');
531
        }
532
533 2
        $i = 0;
534
        // Iterate method arguments
535 2
        foreach ($dependencies as $argument => $dependency) {
536
            // Add dependencies
537 2
            $this->buildResolverArgument($dependency);
538
539
            // Add comma if this is not last dependency
540 2
            if (++$i < $argumentsCount) {
541 2
                $this->generator->text(',');
542
            }
543
        }
544
545 2
        $this->generator->decreaseIndentation()->newLine(');');
546 2
    }
547
}
548