Completed
Push — master ( d8e7c5...849dd2 )
by Vitaly
04:55 queued 01:45
created

Builder::buildResolverArgument()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7.2269

Importance

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