Test Failed
Pull Request — master (#37)
by Divine Niiquaye
02:59
created

ContainerBuilder::doResolveClass()   B

Complexity

Conditions 10
Paths 19

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 10.0454

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 25
ccs 12
cts 13
cp 0.9231
rs 7.6666
cc 10
nc 19
nop 3
crap 10.0454

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI;
19
20
use PhpParser\Node\{Expr, MatchArm, Name, Param, Scalar, Scalar\String_};
21
use PhpParser\Node\Stmt\{Case_, ClassMethod, Declare_, DeclareDeclare, Expression, Nop, Return_};
22
use Rade\DI\Definitions\{DefinitionInterface, ShareableDefinitionInterface};
23
use Rade\DI\Exceptions\ServiceCreationException;
24
use Symfony\Component\Config\Resource\ResourceInterface;
25
26
/**
27
 * A compilable container to build services easily.
28
 *
29
 * Generates a compiled container. This means that there is no runtime performance impact.
30
 *
31
 * @author Divine Niiquaye Ibok <[email protected]>
32
 */
33
class ContainerBuilder extends AbstractContainer
34
{
35
    private const BUILD_SERVICE_DEFINITION = 3;
36
37
    /** @var array<string,ResourceInterface>|null */
38
    private ?array $resources;
39
40
    /** Name of the compiled container parent class. */
41
    private string $containerParentClass;
42
43
    private ?\PhpParser\NodeTraverser $nodeTraverser = null;
44
45
    /**
46
     * Compile the container for optimum performances.
47
     *
48
     * @param string $containerParentClass Name of the compiled container parent class. Customize only if necessary.
49
     */
50
    public function __construct(string $containerParentClass = Container::class)
51
    {
52
        if (!\class_exists(\PhpParser\BuilderFactory::class)) {
53
            throw new \RuntimeException('ContainerBuilder uses "nikic/php-parser" v4, do composer require the nikic/php-parser package.');
54
        }
55
56
        $this->containerParentClass = $c = $containerParentClass;
57
        $this->resources = \interface_exists(ResourceInterface::class) ? [] : null;
58 40
        $this->resolver = new Resolver($this, new \PhpParser\BuilderFactory());
59
        $this->services[self::SERVICE_CONTAINER] = new Expr\Variable('this');
60 40
        $this->type(self::SERVICE_CONTAINER, \array_keys((\class_implements($c) ?: []) + (\class_parents($c) ?: []) + [$c => $c]));
61 40
    }
62
63 40
    /**
64 40
     * {@inheritdoc}
65
     */
66 40
    public function set(string $id, object $definition = null): object
67 40
    {
68 40
        if ($definition instanceof \PhpParser\Node) {
69
            $definition = new Definitions\ValueDefinition($definition);
70
        }
71
72
        return parent::set($id, $definition);
73 17
    }
74
75 17
    /**
76 1
     * Returns an array of resources loaded to build this configuration.
77
     *
78
     * @return ResourceInterface[] An array of resources
79 16
     */
80 16
    public function getResources(): array
81
    {
82 16
        return \array_values($this->resources ?? []);
83
    }
84
85
    /**
86 16
     * Add a resource to allow re-build of container.
87
     *
88
     * @return $this
89
     */
90
    public function addResource(ResourceInterface $resource)
91
    {
92
        if (\is_array($this->resources)) {
93
            $this->resources[(string) $resource] = $resource;
94
        }
95
96
        return $this;
97
    }
98
99
    /**
100 2
     * Add a node visitor to traverse the generated ast.
101
     *
102 2
     * @return $this
103
     */
104 2
    public function addNodeVisitor(\PhpParser\NodeVisitor $nodeVisitor)
105 1
    {
106
        if (null === $this->nodeTraverser) {
107
            $this->nodeTraverser = new \PhpParser\NodeTraverser();
108
        }
109 1
110
        $this->nodeTraverser->addVisitor($nodeVisitor);
111 1
112
        return $this;
113
    }
114
115
    /**
116
     * Compiles the container.
117 17
     * This method main job is to manipulate and optimize the container.
118
     *
119 17
     * supported $options config (defaults):
120
     * - strictType => true,
121
     * - printToString => true,
122
     * - shortArraySyntax => true,
123
     * - maxLineLength => 200,
124
     * - containerClass => CompiledContainer,
125
     *
126
     * @throws \ReflectionException
127
     *
128
     * @return \PhpParser\Node[]|string
129 10
     */
130
    public function compile(array $options = [])
131 10
    {
132 1
        $options += ['strictType' => true, 'printToString' => true, 'containerClass' => 'CompiledContainer'];
133 1
        $astNodes = $options['strictType'] ? [new Declare_([new DeclareDeclare('strict_types', $this->resolver->getBuilder()->val(1))])] : [];
134
135
        $processedData = $this->doAnalyse($this->definitions);
136
        $containerNode = $this->resolver->getBuilder()->class($options['containerClass'])->extend($this->containerParentClass)->setDocComment(Builder\CodePrinter::COMMENT);
137 9
138
        if (!empty($processedData[0])) {
139
            $containerNode->addStmt($this->resolver->getBuilder()->property('aliases')->makeProtected()->setType('array')->setDefault($processedData[0]));
140
        }
141
142
        if (!empty($parameters = $this->parameters)) {
143
            \ksort($parameters);
144
            $this->compileToConstructor($this->resolveParameters($parameters), $containerNode, 'parameters');
145
        }
146
147 49
        if (!empty($processedData[3])) {
148
            $containerNode->addStmt($this->resolver->getBuilder()->property('types')->makeProtected()->setType('array')->setDefault($processedData[3]));
149 49
        }
150
151 49
        if (!empty($processedData[4])) {
152 49
            $containerNode->addStmt($this->resolver->getBuilder()->property('tags')->makeProtected()->setType('array')->setDefault($processedData[4]));
153 22
        }
154
155
        if (!empty($processedData[1])) {
156 49
            $this->compileHasGetMethod($processedData[1], $containerNode);
157
        }
158
        $astNodes[] = $containerNode->addStmts($processedData[2])->getNode();
159 49
160
        if (null !== $this->nodeTraverser) {
161
            $astNodes = $this->nodeTraverser->traverse($astNodes);
162
        }
163
164
        if ($options['printToString']) {
165 17
            unset($options['strictType'], $options['printToString'], $options['containerClass']);
166
167
            return Builder\CodePrinter::print($astNodes, $options);
168 17
        }
169 9
170
        return $astNodes;
171 16
    }
172 12
173
    /**
174 8
     * @param mixed $definition
175 7
     *
176
     * @return mixed
177 2
     */
178 1
    public function dumpObject(string $id, $definition)
179
    {
180
        $method = $this->resolver->getBuilder()->method($this->resolver::createMethod($id))->makeProtected();
181 1
182
        if ($definition instanceof Expression) {
183
            $definition = $definition->expr;
184
        }
185
186
        if ($definition instanceof \PhpParser\Node) {
187
            if ($definition instanceof Expr\Array_) {
188 39
                $method->setReturnType('array');
189
            } elseif ($definition instanceof Expr\New_) {
190 39
                $method->setReturnType($definition->class->toString());
0 ignored issues
show
Bug introduced by
The method toString() does not exist on PhpParser\Node\Stmt\Class_. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

190
                $method->setReturnType($definition->class->/** @scrutinizer ignore-call */ toString());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method toString() does not exist on PhpParser\Node\Expr. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

190
                $method->setReturnType($definition->class->/** @scrutinizer ignore-call */ toString());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
191
            }
192
        } elseif (\is_object($definition)) {
193
            if ($definition instanceof \Closure) {
194
                throw new ServiceCreationException(\sprintf('Cannot dump closure for service "%s".', $id));
195
            }
196 1
197
            if ($definition instanceof \stdClass) {
198 1
                $method->setReturnType('object');
199 1
                $definition = new Expr\Cast\Object_($this->resolver->getBuilder()->val($this->resolver->resolveArguments((array) $definition)));
200
            } elseif ($definition instanceof \IteratorAggregate) {
201
                $method->setReturnType('iterable');
202 1
                $definition = $this->resolver->getBuilder()->new(\get_class($definition), [$this->resolver->resolveArguments(\iterator_to_array($definition))]);
203 1
            } else {
204
                $method->setReturnType(\get_class($definition));
205
                $definition = $this->resolver->getBuilder()->funcCall('\\unserialize', [new String_(\serialize($definition), ['docLabel' => 'SERIALIZED', 'kind' => String_::KIND_NOWDOC])]);
206
            }
207
        }
208 11
209
        $cachedService = new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'services'), new String_($id));
210 11
211
        return $method->addStmt(new \PhpParser\Node\Stmt\Return_(new Expr\Assign($cachedService, $this->resolver->getBuilder()->val($definition))));
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217
    protected function doCreate(string $id, object $definition, int $invalidBehavior)
218 2
    {
219
        if ($definition instanceof String_ && $id === $definition->value) {
220 2
            unset($this->services[$id]);
221
222
            return null;
223
        }
224
225
        $compiledDefinition = $definition instanceof DefinitionInterface ? $definition->build($id, $this->resolver) : $this->dumpObject($id, $definition);
226
227
        if (self::BUILD_SERVICE_DEFINITION !== $invalidBehavior) {
228 51
            $resolved = $this->resolver->getBuilder()->methodCall($this->resolver->getBuilder()->var('this'), $this->resolver::createMethod($id));
229
            $serviceType = 'services';
230 51
231 51
            if ($definition instanceof ShareableDefinitionInterface) {
232
                if (!$definition->isShared()) {
233
                    return $this->services[$id] = $resolved;
234 51
                }
235
236
                if (!$definition->isPublic()) {
237
                    $serviceType = 'privates';
238
                }
239
            }
240
241
            $service = $this->resolver->getBuilder()->propertyFetch($this->resolver->getBuilder()->var('this'), $serviceType);
242
            $createdService = new Expr\BinaryOp\Coalesce(new Expr\ArrayDimFetch($service, new String_($id)), $resolved);
243
244
            return $this->services[$id] = $createdService;
245
        }
246
247
        return $compiledDefinition->getNode();
248
    }
249
250 49
    /**
251
     * Analyse all definitions, build definitions and return results.
252 49
     *
253
     * @param DefinitionInterface[] $definitions
254
     */
255
    protected function doAnalyse(array $definitions, bool $onlyDefinitions = false): array
256
    {
257
        $methodsMap = $serviceMethods = $wiredTypes = [];
258
        $s = $this->services[self::SERVICE_CONTAINER] ?? new Expr\Variable('this');
259
260
        if (!isset($methodsMap[self::SERVICE_CONTAINER])) {
261
            $methodsMap[self::SERVICE_CONTAINER] = 80000 <= \PHP_VERSION_ID ? new MatchArm([new String_(self::SERVICE_CONTAINER)], $s) : new Case_(new String_(self::SERVICE_CONTAINER), [$s]);
262
        }
263
264
        foreach ($definitions as $id => $definition) {
265
            if ($this->tagged('container.remove_services', $id)) {
266
                continue;
267
            }
268
269
            $m = ($b= $this->resolver->getBuilder())->methodCall($s, $this->resolver::createMethod($id));
0 ignored issues
show
Bug introduced by
The method methodCall() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

269
            /** @scrutinizer ignore-call */ 
270
            $m = ($b= $this->resolver->getBuilder())->methodCall($s, $this->resolver::createMethod($id));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Coding Style introduced by
Expected at least 1 space before "="; 0 found
Loading history...
270 12
            $methodsMap[$id] = 80000 <= \PHP_VERSION_ID ? new MatchArm([$si = new String_($id)], $m) : new Case_($si = new String_($id), [new Return_($m)]);
271
272 12
            if ($definition instanceof ShareableDefinitionInterface) {
273 12
                if (!$definition->isPublic()) {
274
                    unset($methodsMap[$id]);
275 12
                } elseif ($definition->isShared()) {
276 1
                    $sr = new Expr\ArrayDimFetch($b->propertyFetch($s, 'services'), $si);
277 1
                    $sb = &$methodsMap[$id];
278 1
                    $sb instanceof MatchArm ? $sb->body = new Expr\BinaryOp\Coalesce($sr, $sb->body) : $sb->stmts[0]->expr = new Expr\BinaryOp\Coalesce($sr, $sb->stmts[0]->expr);
279 1
                }
280
281
                if ($definition->isAbstract()) {
282 1
                    unset($methodsMap[$id]);
283 1
                    continue;
284
                }
285
            }
286
287 12
            $serviceMethods[] = $this->doCreate($id, $definition, self::BUILD_SERVICE_DEFINITION);
288 12
        }
289
290
        if ($onlyDefinitions) {
291 12
            return [$methodsMap, $serviceMethods];
292 12
        }
293
294 12
        if ($newDefinitions = \array_diff_key($this->definitions, $definitions)) {
295 12
            $processedData = $this->doAnalyse($newDefinitions, true);
296
            $methodsMap = \array_merge($methodsMap, $processedData[0]);
297
            $serviceMethods = [...$serviceMethods, ...$processedData[1]];
298 1
        }
299
300
        $aliases = \array_filter($this->aliases, static fn (string $aliased): bool => isset($methodsMap[$aliased]));
301
        $tags = \array_filter($this->tags, static fn (array $tagged): bool => isset($methodsMap[\key($tagged)]));
302
303
        // Prevent autowired private services from be exported.
304
        foreach ($this->types as $type => $ids) {
305
            $ids = \array_filter($ids, static fn (string $id): bool => isset($methodsMap[$id]));
306 18
307
            if ([] !== $ids) {
308 18
                $ids = \array_values($ids); // If $ids are filtered, keys should not be preserved.
309 6
                $wiredTypes[] = new Expr\ArrayItem($this->resolver->getBuilder()->val($ids), new String_($type));
310
            }
311
        }
312
313 18
        \natsort($aliases);
314
        \ksort($methodsMap);
315 18
        \ksort($tags, \SORT_NATURAL);
316 3
        \usort($serviceMethods, fn (ClassMethod $a, ClassMethod $b): int => \strnatcmp($a->name->toString(), $b->name->toString()));
317
        \usort($wiredTypes, fn (Expr\ArrayItem $a, Expr\ArrayItem $b): int => \strnatcmp($a->key->value, $b->key->value));
318
319
        return [$aliases, $methodsMap, $serviceMethods, $wiredTypes, $tags];
320 18
    }
321
322 12
    /**
323
     * Build parameters + dynamic parameters in compiled container class.
324 18
     *
325
     * @param array<int,array<string,mixed>> $parameters
326
     */
327
    protected function compileToConstructor(array $parameters, \PhpParser\Builder\Class_ &$containerNode, string $name): void
328
    {
329
        [$resolvedParameters, $dynamicParameters] = $parameters;
330
331 12
        if (!empty($dynamicParameters)) {
332
            $resolver = $this->resolver;
333 12
            $container = $this->containerParentClass;
334 12
            $containerNode = \Closure::bind(function (\PhpParser\Builder\Class_ $node) use ($dynamicParameters, $resolver, $container, $name) {
335
                $endMethod = \array_pop($node->methods);
0 ignored issues
show
Bug introduced by
The property methods is declared protected in PhpParser\Builder\Class_ and cannot be accessed from this context.
Loading history...
336 12
                $constructorNode = $resolver->getBuilder()->method('__construct');
337
338 3
                if ($endMethod instanceof ClassMethod && '__construct' === $endMethod->name->name) {
339 3
                    $constructorNode->addStmts([...$endMethod->stmts, new Nop()]);
340 3
                } elseif (\method_exists($container, '__construct')) {
341 3
                    $constructorNode->addStmt($resolver->getBuilder()->staticCall(new Name('parent'), '__construct'));
342
                }
343
344
                foreach ($dynamicParameters as $offset => $value) {
345
                    $parameter = $resolver->getBuilder()->propertyFetch($resolver->getBuilder()->var('this'), $name);
346 12
                    $constructorNode->addStmt(new Expr\Assign(new Expr\ArrayDimFetch($parameter, new String_($offset)), $resolver->getBuilder()->val($value)));
347 12
                }
348 12
349 12
                return $node->addStmt($constructorNode->makePublic());
350 12
            }, $containerNode, $containerNode)($containerNode);
351 12
        }
352 12
353 12
        if (!empty($resolvedParameters)) {
354 12
            $containerNode->addStmt($this->resolver->getBuilder()->property($name)->makePublic()->setType('array')->setDefault($resolvedParameters));
355 12
        }
356 12
    }
357 12
358 12
    /**
359 12
     * Build the container's get method.
360
     */
361
    protected function compileHasGetMethod(array $getMethods, \PhpParser\Builder\Class_ &$containerNode): void
362
    {
363
        if (!\method_exists($this->containerParentClass, 'doLoad')) {
364
            throw new ServiceCreationException(\sprintf('The %c class must have a "doLoad" protected method', $this->containerParentClass));
365
        }
366
367
        if (!\method_exists($this->containerParentClass, 'has')) {
368 12
            throw new ServiceCreationException(\sprintf('The %c class must have a "has" public method', $this->containerParentClass));
369
        }
370 12
371 12
        $p8 = 80000 <= \PHP_VERSION_ID;
372
        $s = $this->services[self::SERVICE_CONTAINER] ?? new Expr\Variable('this');
373 12
374 11
        if ($p8) {
375
            $getMethods[] = $md = new MatchArm([new Expr\ConstFetch(new Name('default'))], new Expr\ConstFetch(new Name('null')));
376 11
        }
377 3
        $getNode = ($b = $this->resolver->getBuilder())->method('get')->makePublic();
378
        $hasNode = $b->method('has')->makePublic()->setReturnType('bool');
379 3
        $ia = new Expr\Assign(
380
            $i = new Expr\Variable('id'),
381
            $ii = new Expr\BinaryOp\Coalesce(new Expr\ArrayDimFetch($b->propertyFetch($s, 'aliases'), $i), $i)
382 10
        );
383
        $hasNode->addParam($mi = new Param($i, null, 'string'));
384
        $getNode->addParams([$mi, new Param($ib = new Expr\Variable('invalidBehavior'), $b->val(1), 'int')]);
385
        $getNode->addStmt($p8 ? new Expr\Assign($sv = new Expr\Variable('s'), new Expr\Match_($ia, $getMethods)) : new \PhpParser\Node\Stmt\Switch_($ia, $getMethods));
386 12
        $sf = new Expr\BinaryOp\Coalesce(new Expr\ArrayDimFetch($b->propertyFetch($s, 'services'), $i), $b->methodCall($s, 'doLoad', [$i, $ib]));
387 2
        $hf = $b->staticCall('parent', 'has', [$i]);
388 1
389
        if ($p8) {
390
            unset($getMethods[0]);
391
            $hasNode->addStmt(new Expr\Assign($sv, new Expr\Match_($i, [new MatchArm(\array_map([$b, 'val'], \array_keys($getMethods)), $b->val(true)), $md])));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $md does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $sv does not seem to be defined for all execution paths leading up to this point.
Loading history...
392
            $hf = new Expr\BinaryOp\Coalesce($sv, $hf);
393 12
            $sf = new Expr\BinaryOp\Coalesce($sv, $sf);
394 12
        } else {
395 1
            $hf = new Expr\BinaryOp\BooleanOr($b->funcCall('method_exists', [$s, $b->staticCall(Resolver::class, 'createMethod', [$ii])]), $hf);
396
        }
397
398 12
        $containerNode->addStmt($hasNode->addStmt(new Return_($hf)));
399 12
        $containerNode->addStmt($getNode->addStmt(new Return_($sf)));
400
    }
401 12
402
    /**
403
     * Resolve parameter's and retrieve dynamic type parameter.
404 12
     *
405
     * @param array<string,mixed> $parameters
406
     *
407
     * @return array<int,mixed>
408
     */
409
    protected function resolveParameters(array $parameters, bool $recursive = false): array
410 12
    {
411
        $resolvedParameters = $dynamicParameters = [];
412 12
413
        if (!$recursive) {
414
            $parameters = $this->resolver->resolveArguments($parameters);
415
        }
416
417
        foreach ($parameters as $parameter => $value) {
418 16
            if (\is_array($value)) {
419
                $arrayParameters = $this->resolveParameters($value, $recursive);
420 16
421 8
                if (!empty($arrayParameters[1])) {
422
                    $grouped = $arrayParameters[1] + $arrayParameters[0];
423 11
                    \uksort($grouped, fn ($a, $b) => (\is_int($a) && \is_int($b) ? $a <=> $b : 0));
424
                    $dynamicParameters[$parameter] = $grouped;
425
                } else {
426
                    $resolvedParameters[$parameter] = $arrayParameters[0];
427 11
                }
428
429
                continue;
430 14
            }
431 1
432 1
            if ($value instanceof Scalar || $value instanceof Expr\ConstFetch) {
433
                $resolvedParameters[$parameter] = $value;
434
435
                continue;
436 14
            }
437 6
438 1
            $dynamicParameters[$parameter] = $value;
439
        }
440
441
        return [$resolvedParameters, $dynamicParameters];
442 14
    }
443
}
444