Test Failed
Pull Request — master (#37)
by Divine Niiquaye
02:55
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, Name, Scalar, Scalar\String_};
21
use PhpParser\Node\Stmt\{ClassMethod, Declare_, DeclareDeclare, Expression, Nop};
22
use Rade\DI\Definitions\{DefinitionInterface, ShareableDefinitionInterface};
23
use Rade\DI\Exceptions\ServiceCreationException;
24
use Symfony\Component\Config\Resource\ResourceInterface;
25
use Symfony\Component\VarExporter\VarExporter;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\VarExporter\VarExporter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
27
/**
28
 * A compilable container to build services easily.
29
 *
30
 * Generates a compiled container. This means that there is no runtime performance impact.
31
 *
32
 * @author Divine Niiquaye Ibok <[email protected]>
33
 */
34
class ContainerBuilder extends AbstractContainer
35
{
36
    public const BUILD_SERVICE_DEFINITION = 3;
37
38
    /** @var array<string,ResourceInterface>|null */
39
    private ?array $resources;
40
41
    /** Name of the compiled container parent class. */
42
    private string $containerParentClass;
43
44
    private ?\PhpParser\NodeTraverser $nodeTraverser = null;
45
46
    /**
47
     * Compile the container for optimum performances.
48
     *
49
     * @param string $containerParentClass Name of the compiled container parent class. Customize only if necessary.
50
     */
51
    public function __construct(string $containerParentClass = Container::class)
52
    {
53
        if (!\class_exists(\PhpParser\BuilderFactory::class)) {
54
            throw new \RuntimeException('ContainerBuilder uses "nikic/php-parser" v4, do composer require the nikic/php-parser package.');
55
        }
56
57
        $this->containerParentClass = $c = $containerParentClass;
58 40
        $this->resources = \interface_exists(ResourceInterface::class) ? [] : null;
59
        $this->resolver = new Resolver($this, new \PhpParser\BuilderFactory());
60 40
        $this->services[self::SERVICE_CONTAINER] = new Expr\Variable('this');
61 40
        $this->type(self::SERVICE_CONTAINER, \array_keys((\class_implements($c) ?: []) + (\class_parents($c) ?: []) + [$c => $c]));
62
    }
63 40
64 40
    /**
65
     * {@inheritdoc}
66 40
     */
67 40
    public function set(string $id, object $definition = null): object
68 40
    {
69
        if ($definition instanceof \PhpParser\Node) {
70
            $definition = new Definitions\ValueDefinition($definition);
71
        }
72
73 17
        return parent::set($id, $definition);
74
    }
75 17
76 1
    /**
77
     * {@inheritdoc}
78
     */
79 16
    public function reset(): void
80 16
    {
81
        parent::reset();
82 16
        $this->nodeTraverser = null;
83
84
        if (isset($this->resources)) {
85
            $this->resources = [];
86 16
        }
87
    }
88
89
    /**
90
     * Returns an array of resources loaded to build this configuration.
91
     *
92
     * @return ResourceInterface[] An array of resources
93
     */
94
    public function getResources(): array
95
    {
96
        return \array_values($this->resources ?? []);
97
    }
98
99
    /**
100 2
     * Add a resource to allow re-build of container.
101
     *
102 2
     * @return $this
103
     */
104 2
    public function addResource(ResourceInterface $resource)
105 1
    {
106
        if (\is_array($this->resources)) {
107
            $this->resources[(string) $resource] = $resource;
108
        }
109 1
110
        return $this;
111 1
    }
112
113
    /**
114
     * Add a node visitor to traverse the generated ast.
115
     *
116
     * @return $this
117 17
     */
118
    public function addNodeVisitor(\PhpParser\NodeVisitor $nodeVisitor)
119 17
    {
120
        if (null === $this->nodeTraverser) {
121
            $this->nodeTraverser = new \PhpParser\NodeTraverser();
122
        }
123
124
        $this->nodeTraverser->addVisitor($nodeVisitor);
125
126
        return $this;
127
    }
128
129 10
    /**
130
     * Compiles the container.
131 10
     * This method main job is to manipulate and optimize the container.
132 1
     *
133 1
     * supported $options config (defaults):
134
     * - strictType => true,
135
     * - printToString => true,
136
     * - shortArraySyntax => true,
137 9
     * - maxLineLength => 200,
138
     * - containerClass => CompiledContainer,
139
     *
140
     * @throws \ReflectionException
141
     *
142
     * @return \PhpParser\Node[]|string
143
     */
144
    public function compile(array $options = [])
145
    {
146
        $options += ['strictType' => true, 'printToString' => true, 'containerClass' => 'CompiledContainer'];
147 49
        $astNodes = $options['strictType'] ? [new Declare_([new DeclareDeclare('strict_types', $this->resolver->getBuilder()->val(1))])] : [];
148
149 49
        $processedData = $this->doAnalyse($this->definitions);
150
        $containerNode = $this->resolver->getBuilder()->class($options['containerClass'])->extend($this->containerParentClass)->setDocComment(Builder\CodePrinter::COMMENT);
151 49
152 49
        if (!empty($processedData[0])) {
153 22
            $containerNode->addStmt($this->resolver->getBuilder()->property('aliases')->makeProtected()->setType('array')->setDefault($processedData[0]));
154
        }
155
156 49
        if (!empty($parameters = $this->parameters)) {
157
            \ksort($parameters);
158
            $this->compileToConstructor($this->resolveParameters($parameters), $containerNode, 'parameters');
159 49
        }
160
161
        if (!empty($processedData[1]) && count($processedData[1]) > 1) {
162
            unset($processedData[1][self::SERVICE_CONTAINER]);
163
            $containerNode->addStmt($this->resolver->getBuilder()->property('methodsMap')->makeProtected()->setType('array')->setDefault($processedData[1]));
164
        }
165 17
166
        if (!empty($processedData[3])) {
167
            $containerNode->addStmt($this->resolver->getBuilder()->property('types')->makeProtected()->setType('array')->setDefault($processedData[3]));
168 17
        }
169 9
170
        if (!empty($processedData[4])) {
171 16
            $containerNode->addStmt($this->resolver->getBuilder()->property('tags')->makeProtected()->setType('array')->setDefault($processedData[4]));
172 12
        }
173
174 8
        if (!empty($processedData[2])) {
175 7
            $containerNode->addStmts($processedData[2]);
176
        }
177 2
178 1
        $astNodes[] = $containerNode->getNode(); // Build the container class
179
180
        if (null !== $this->nodeTraverser) {
181 1
            $astNodes = $this->nodeTraverser->traverse($astNodes);
182
        }
183
184
        if ($options['printToString']) {
185
            unset($options['strictType'], $options['printToString'], $options['containerClass']);
186
187
            return Builder\CodePrinter::print($astNodes, $options);
188 39
        }
189
190 39
        return $astNodes;
191
    }
192
193
    /**
194
     * @param mixed $definition
195
     *
196 1
     * @return mixed
197
     */
198 1
    public function dumpObject(string $id, $definition)
199 1
    {
200
        $method = $this->resolver->getBuilder()->method($this->resolver::createMethod($id))->makeProtected();
201
        $cachedService = new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'services'), new String_($id));
202 1
203 1
        if ($definition instanceof Expression) {
204
            $definition = $definition->expr;
205
        }
206
207
        if ($definition instanceof \PhpParser\Node) {
208 11
            if ($definition instanceof Expr\Array_) {
209
                $method->setReturnType('array');
210 11
            } elseif ($definition instanceof Expr\New_) {
211
                $method->setReturnType($definition->class->toString());
0 ignored issues
show
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

211
                $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\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

211
                $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...
212
            }
213
        } elseif (\is_object($definition)) {
214
            if ($definition instanceof \Closure) {
215
                throw new ServiceCreationException(\sprintf('Cannot dump closure for service "%s".', $id));
216
            }
217
218 2
            if ($definition instanceof \stdClass) {
219
                $method->setReturnType('object');
220 2
                $definition = new Expr\Cast\Object_($this->resolver->getBuilder()->val($this->resolver->resolveArguments((array) $definition)));
221
            } elseif ($definition instanceof \IteratorAggregate) {
222
                $method->setReturnType('iterable');
223
                $definition = $this->resolver->getBuilder()->new(\get_class($definition), [$this->resolver->resolveArguments(\iterator_to_array($definition))]);
224
            } else {
225
                $method->setReturnType(\get_class($definition));
226
227
                if (\class_exists(VarExporter::class)) {
228 51
                    $definition = Loader\phpCode(VarExporter::export($definition))->resolve($this->resolver);
229
                } else {
230 51
                    $definition = $this->resolver->getBuilder()->funcCall('\\unserialize', [new String_(\serialize($definition), ['docLabel' => 'SERIALIZED', 'kind' => String_::KIND_NOWDOC])]);
231 51
                }
232
            }
233
        }
234 51
235
        return $method->addStmt(new \PhpParser\Node\Stmt\Return_(new Expr\Assign($cachedService, $this->resolver->getBuilder()->val($definition))));
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241
    protected function doCreate(string $id, $definition, int $invalidBehavior)
242
    {
243
        if ($definition instanceof String_ && $id === $definition->value) {
244
            unset($this->services[$id]);
245
246
            return null;
247
        }
248
249
        $compiledDefinition = $definition instanceof DefinitionInterface ? $definition->build($id, $this->resolver) : $this->dumpObject($id, $definition);
250 49
251
        if (self::BUILD_SERVICE_DEFINITION !== $invalidBehavior) {
252 49
            $resolved = $this->resolver->getBuilder()->methodCall($this->resolver->getBuilder()->var('this'), $this->resolver::createMethod($id));
253
            $serviceType = 'services';
254
255
            if ($definition instanceof ShareableDefinitionInterface) {
256
                if (!$definition->isShared()) {
257
                    return $this->services[$id] = $resolved;
258
                }
259
260
                if (!$definition->isPublic()) {
261
                    $serviceType = 'privates';
262
                }
263
            }
264
265
            $service = $this->resolver->getBuilder()->propertyFetch($this->resolver->getBuilder()->var('this'), $serviceType);
266
            $createdService = new Expr\BinaryOp\Coalesce(new Expr\ArrayDimFetch($service, new String_($id)), $resolved);
267
268
            return $this->services[$id] = $createdService;
269
        }
270 12
271
        return $compiledDefinition->getNode();
272 12
    }
273 12
274
    /**
275 12
     * Analyse all definitions, build definitions and return results.
276 1
     *
277 1
     * @param DefinitionInterface[] $definitions
278 1
     */
279 1
    protected function doAnalyse(array $definitions, bool $onlyDefinitions = false): array
280
    {
281
        $methodsMap = $serviceMethods = $wiredTypes = [];
282 1
283 1
        if (!isset($methodsMap[self::SERVICE_CONTAINER])) {
284
            $methodsMap[self::SERVICE_CONTAINER] = true;
285
        }
286
287 12
        foreach ($definitions as $id => $definition) {
288 12
            if ($this->tagged('container.remove_services', $id)) {
289
                continue;
290
            }
291 12
292 12
            $methodsMap[$id] = $this->resolver::createMethod($id);
293
294 12
            if ($definition instanceof ShareableDefinitionInterface) {
295 12
                if (!$definition->isPublic()) {
296
                    unset($methodsMap[$id]);
297
                }
298 1
299
                if ($definition->isAbstract()) {
300
                    unset($methodsMap[$id]);
301
                    continue;
302
                }
303
            }
304
305
            $serviceMethods[] = $this->doCreate($id, $definition, self::BUILD_SERVICE_DEFINITION);
306 18
        }
307
308 18
        if ($onlyDefinitions) {
309 6
            return [$methodsMap, $serviceMethods];
310
        }
311
312
        if ($newDefinitions = \array_diff_key($this->definitions, $definitions)) {
313 18
            $processedData = $this->doAnalyse($newDefinitions, true);
314
            $methodsMap = \array_merge($methodsMap, $processedData[0]);
315 18
            $serviceMethods = [...$serviceMethods, ...$processedData[1]];
316 3
        }
317
318
        $aliases = \array_filter($this->aliases, static fn (string $aliased): bool => isset($methodsMap[$aliased]));
319
        $tags = \array_filter($this->tags, static fn (array $tagged): bool => isset($methodsMap[\key($tagged)]));
320 18
321
        // Prevent autowired private services from be exported.
322 12
        foreach ($this->types as $type => $ids) {
323
            $ids = \array_filter($ids, static fn (string $id): bool => isset($methodsMap[$id]));
324 18
325
            if ([] !== $ids) {
326
                $ids = \array_values($ids); // If $ids are filtered, keys should not be preserved.
327
                $wiredTypes[] = new Expr\ArrayItem($this->resolver->getBuilder()->val($ids), new String_($type));
328
            }
329
        }
330
331 12
        \natsort($aliases);
332
        \ksort($methodsMap);
333 12
        \ksort($tags, \SORT_NATURAL);
334 12
        \usort($serviceMethods, fn (ClassMethod $a, ClassMethod $b): int => \strnatcmp($a->name->toString(), $b->name->toString()));
335
        \usort($wiredTypes, fn (Expr\ArrayItem $a, Expr\ArrayItem $b): int => \strnatcmp($a->key->value, $b->key->value));
336 12
337
        return [$aliases, $methodsMap, $serviceMethods, $wiredTypes, $tags];
338 3
    }
339 3
340 3
    /**
341 3
     * Build parameters + dynamic parameters in compiled container class.
342
     *
343
     * @param array<int,array<string,mixed>> $parameters
344
     */
345
    protected function compileToConstructor(array $parameters, \PhpParser\Builder\Class_ &$containerNode, string $name): void
346 12
    {
347 12
        [$resolvedParameters, $dynamicParameters] = $parameters;
348 12
349 12
        if (!empty($dynamicParameters)) {
350 12
            $resolver = $this->resolver;
351 12
            $container = $this->containerParentClass;
352 12
            $containerNode = \Closure::bind(function (\PhpParser\Builder\Class_ $node) use ($dynamicParameters, $resolver, $container, $name) {
353 12
                $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...
354 12
                $constructorNode = $resolver->getBuilder()->method('__construct');
355 12
356 12
                if ($endMethod instanceof ClassMethod && '__construct' === $endMethod->name->name) {
357 12
                    $constructorNode->addStmts([...$endMethod->stmts, new Nop()]);
358 12
                } elseif (\method_exists($container, '__construct')) {
359 12
                    $constructorNode->addStmt($resolver->getBuilder()->staticCall(new Name('parent'), '__construct'));
360
                }
361
362
                foreach ($dynamicParameters as $offset => $value) {
363
                    $parameter = $resolver->getBuilder()->propertyFetch($resolver->getBuilder()->var('this'), $name);
364
                    $constructorNode->addStmt(new Expr\Assign(new Expr\ArrayDimFetch($parameter, new String_($offset)), $resolver->getBuilder()->val($value)));
365
                }
366
367
                return $node->addStmt($constructorNode->makePublic());
368 12
            }, $containerNode, $containerNode)($containerNode);
369
        }
370 12
371 12
        if (!empty($resolvedParameters)) {
372
            $containerNode->addStmt($this->resolver->getBuilder()->property($name)->makePublic()->setType('array')->setDefault($resolvedParameters));
373 12
        }
374 11
    }
375
376 11
    /**
377 3
     * Resolve parameter's and retrieve dynamic type parameter.
378
     *
379 3
     * @param array<string,mixed> $parameters
380
     *
381
     * @return array<int,mixed>
382 10
     */
383
    protected function resolveParameters(array $parameters, bool $recursive = false): array
384
    {
385
        $resolvedParameters = $dynamicParameters = [];
386 12
387 2
        if (!$recursive) {
388 1
            $parameters = $this->resolver->resolveArguments($parameters);
389
        }
390
391
        foreach ($parameters as $parameter => $value) {
392
            if (\is_array($value)) {
393 12
                $arrayParameters = $this->resolveParameters($value, $recursive);
394 12
395 1
                if (!empty($arrayParameters[1])) {
396
                    $grouped = $arrayParameters[1] + $arrayParameters[0];
397
                    \uksort($grouped, fn ($a, $b) => (\is_int($a) && \is_int($b) ? $a <=> $b : 0));
398 12
                    $dynamicParameters[$parameter] = $grouped;
399 12
                } else {
400
                    $resolvedParameters[$parameter] = $arrayParameters[0];
401 12
                }
402
403
                continue;
404 12
            }
405
406
            if ($value instanceof Scalar || $value instanceof Expr\ConstFetch) {
407
                $resolvedParameters[$parameter] = $value;
408
409
                continue;
410 12
            }
411
412 12
            $dynamicParameters[$parameter] = $value;
413
        }
414
415
        return [$resolvedParameters, $dynamicParameters];
416
    }
417
}
418