Test Failed
Push — master ( 42f43f...2991f1 )
by Divine Niiquaye
03:24
created

ContainerBuilder::getDefinitions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 1
cts 1
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
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\{Name, Expr\ArrayItem, Expr\Assign, Expr\Variable, Expr\StaticPropertyFetch};
21
use PhpParser\Node\Stmt\{Declare_, DeclareDeclare};
22
use Psr\Container\ContainerInterface;
23
use Rade\DI\{
24
    Builder\Statement,
25
    Exceptions\CircularReferenceException,
26
    Exceptions\NotFoundServiceException,
27
    Exceptions\ServiceCreationException
28
};
29
use Symfony\Component\Config\{
30
    Resource\ClassExistenceResource,
31
    Resource\FileResource,
32
    Resource\FileExistenceResource,
33
    Resource\ResourceInterface
34
};
35
36
class ContainerBuilder extends AbstractContainer
37
{
38
    private bool $trackResources;
39
40
    private int $hasPrivateServices = 0;
41
42
    /** @var ResourceInterface[] */
43
    private array $resources = [];
44
45
    /** @var Definition[]|RawDefinition[] */
46
    private array $definitions = [];
47
48
    /** Name of the compiled container parent class. */
49
    private string $containerParentClass;
50
51
    private \PhpParser\BuilderFactory $builder;
52
53
    /**
54
     * Compile the container for optimum performances.
55
     *
56
     * @param string $containerParentClass Name of the compiled container parent class. Customize only if necessary.
57
     */
58 39
    public function __construct(string $containerParentClass = Container::class)
59
    {
60 39
        $this->containerParentClass = $containerParentClass;
61 39
        $this->trackResources = \interface_exists(ResourceInterface::class);
62
63 39
        $this->builder = new \PhpParser\BuilderFactory();
64 39
        $this->resolver = new Resolvers\Resolver($this);
65
66 39
        self::$services = ['container' => new Variable('this')];
67 39
        $this->type('container', [ContainerInterface::class, $containerParentClass]);
68 39
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73 16
    public function __call(string $name, array $args)
74
    {
75 16
        if ('call' === $name) {
76 1
            throw new ServiceCreationException(\sprintf('Refactor your code to use %s class instead.', Statement::class));
77
        }
78
79 15
        if ('resolveClass' === $name) {
80 15
            $class = new \ReflectionClass($service = $args[0]);
81
82 15
            if ($class->isAbstract() || !$class->isInstantiable()) {
83
                throw new ServiceCreationException(\sprintf('Class entity %s is an abstract type or instantiable.', $service));
84
            }
85
86 15
            if ((null !== $constructor = $class->getConstructor()) && $constructor->isPublic()) {
87 7
                return $this->builder->new($service, $this->resolver->autowireArguments($constructor, $args[1] ?? []));
88
            }
89
90 10
            if (!empty($args[1] ?? [])) {
91
                throw new ServiceCreationException("Unable to pass arguments, class $service has no constructor or constructor is not public.");
92
            }
93
94 10
            return $this->builder->new($service);
95
        }
96
97
        return parent::__call($name, $args);
98
    }
99
100
    /**
101
     * Extends an object definition.
102
     *
103
     * @param string $id The unique identifier for the definition
104
     *
105
     * @throws NotFoundServiceException If the identifier is not defined
106
     * @throws ServiceCreationException if the definition is a raw type
107
     */
108 2
    public function extend(string $id): Definition
109
    {
110 2
        $extended = $this->definitions[$id] ?? $this->createNotFound($id, true);
111
112 2
        if ($extended instanceof RawDefinition) {
113 1
            throw new ServiceCreationException(\sprintf('Extending a raw definition for "%s" is not supported.', $id));
114
        }
115
116
        // Incase service has been cached, remove it.
117 1
        unset(self::$services[$id]);
118
119 1
        return $this->definitions[$id] = $extended;
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125 17
    public function service(string $id)
126
    {
127 17
        return $this->definitions[$this->aliases[$id] ?? $id] ?? $this->createNotFound($id, true);
128
    }
129
130
    /**
131
     * Sets a autowired service definition.
132
     *
133
     * @param string|array|Definition|Statement $definition
134
     *
135
     * @throws ServiceCreationException If $definition instanceof RawDefinition
136
     */
137 9
    public function autowire(string $id, $definition): Definition
138
    {
139 9
        if ($definition instanceof RawDefinition) {
0 ignored issues
show
introduced by
$definition is never a sub-type of Rade\DI\RawDefinition.
Loading history...
140 1
            throw new ServiceCreationException(
141 1
                \sprintf('Service "%s" using "%s" instance is not supported for autowiring.', $id, RawDefinition::class)
142
            );
143
        }
144
145 8
        return $this->set($id, $definition)->autowire();
146
    }
147
148
    /**
149
     * Sets a service definition.
150
     *
151
     * @param string|array|Definition|Statement|RawDefinition $definition
152
     *
153
     * @return Definition|RawDefinition the service definition
154
     */
155 48
    public function set(string $id, $definition)
156
    {
157 48
        unset($this->aliases[$id]);
158
159 48
        if (!$definition instanceof RawDefinition) {
160 48
            if (!$definition instanceof Definition) {
161 21
                $definition = new Definition($definition);
162
            }
163
164 48
            $definition->withContainer($id, $this);
165
        }
166
167 48
        return $this->definitions[$id] = $definition;
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173 16
    public function get(string $id, int $invalidBehavior = /* self::EXCEPTION_ON_MULTIPLE_SERVICE */ 1)
174
    {
175
        switch (true) {
176 16
            case isset(self::$services[$id]):
177 8
                return self::$services[$id];
178
179 15
            case isset($this->definitions[$id]):
180 11
                return self::$services[$id] = $this->doCreate($id, $this->definitions[$id]);
181
182 7
            case $this->typed($id):
183 6
                return $this->autowired($id, self::EXCEPTION_ON_MULTIPLE_SERVICE === $invalidBehavior);
184
185 2
            case isset($this->aliases[$id]):
186 1
                return $this->get($this->aliases[$id]);
187
188
            default:
189 1
                throw $this->createNotFound($id);
190
        }
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196 38
    public function has(string $id): bool
197
    {
198 38
        return isset($this->definitions[$id]) || ($this->typed($id) || isset($this->aliases[$id]));
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204 1
    public function remove(string $id): void
205
    {
206 1
        if (isset($this->definitions[$id])) {
207 1
            unset($this->definitions[$id]);
208
        }
209
210 1
        parent::remove($id);
211 1
    }
212
213
    /**
214
     * {@inheritdoc}
215
     */
216 11
    public function keys(): array
217
    {
218 11
        return \array_keys($this->definitions);
219
    }
220
221
    /**
222
     * Returns an array of resources loaded to build this configuration.
223
     *
224
     * @return ResourceInterface[] An array of resources
225
     */
226 2
    public function getResources(): array
227
    {
228 2
        return \array_values($this->resources);
229
    }
230
231
    /**
232
     * Add a resource to to allow re-build of container.
233
     *
234
     * @return $this
235
     */
236 51
    public function addResource(ResourceInterface $resource): self
237
    {
238 51
        if ($this->trackResources) {
239 51
            $this->resources[(string) $resource] = $resource;
240
        }
241
242 51
        return $this;
243
    }
244
245
    /**
246
     * Return all service definitions.
247
     *
248 48
     * @return Definition[]|RawDefinition[]
249
     */
250 48
    public function getDefinitions(): array
251
    {
252
        return $this->definitions;
253
    }
254
255
    /**
256
     * Get the builder use to compiler container.
257
     */
258
    public function getBuilder(): \PhpParser\BuilderFactory
259
    {
260
        return $this->builder;
261
    }
262
263
    /**
264
     * Compiles the container.
265
     * This method main job is to manipulate and optimize the container.
266
     *
267
     * supported $options config (defaults):
268 11
     * - strictType => true,
269
     * - printToString => true,
270 11
     * - shortArraySyntax => true,
271 11
     * - spacingLevel => 8,
272
     * - containerClass => CompiledContainer,
273 11
     *
274 1
     * @throws \ReflectionException
275 1
     *
276 1
     * @return \PhpParser\Node[]|string
277 1
     */
278
    public function compile(array $options = [])
279
    {
280 1
        $options += ['strictType' => true, 'printToString' => true, 'containerClass' => 'CompiledContainer'];
281 1
        $astNodes = [];
282
283
        foreach ($this->providers as $name => $builder) {
284
            if ($this->trackResources) {
285 11
                $this->addResource(new ClassExistenceResource($name, false));
286 11
                $this->addResource(new FileExistenceResource($rPath = (new \ReflectionClass($name))->getFileName()));
287
                $this->addResource(new FileResource($rPath));
288
            }
289 11
290 11
            if ($builder instanceof Builder\PrependInterface) {
291
                $builder->before($this);
292 11
            }
293 11
        }
294
295
        if ($options['strictType']) {
296 1
            $astNodes[] = new Declare_([new DeclareDeclare('strict_types', $this->builder->val(1))]);
297
        }
298
299
        $parameters = \array_map(fn ($value) => $this->builder->val($value), $this->parameters);
300
        $astNodes[] = $this->doCompile($this->definitions, $parameters, $options['containerClass'])->getNode();
301
302
        if ($options['printToString']) {
303
            return Builder\CodePrinter::print($astNodes, $options);
304 17
        }
305
306 17
        return $astNodes;
307 6
    }
308
309
    /**
310
     * {@inheritdoc}
311 17
     *
312
     * @param Definition|RawDefinition $service
313 17
     */
314 3
    protected function doCreate(string $id, $service, bool $build = false)
315
    {
316
        if (isset($this->loading[$id])) {
317
            throw new CircularReferenceException($id, [...\array_keys($this->loading), $id]);
318 17
        }
319
320 11
        try {
321
            $this->loading[$id] = true;
322 17
323
            if ($service instanceof RawDefinition) {
324
                return $build ? $service->build($id, $this->builder) : $this->builder->val($service());
325
            }
326
327
            // Strict circular reference check ...
328
            $compiled = $service->build();
329 11
330
            return $build ? $compiled : $service->resolve();
331 11
        } finally {
332 11
            unset($this->loading[$id]);
333
        }
334 11
    }
335
336 3
    /**
337 3
     * @param Definition[]|RawDefinition[] $definitions
338 3
     */
339 3
    protected function doCompile(array $definitions, array $parameters, string $containerClass): \PhpParser\Builder\Class_
340
    {
341
        [$methodsMap, $serviceMethods, $wiredTypes] = $this->doAnalyse($definitions);
342
        $compiledContainerNode = $this->builder->class($containerClass)->extend($this->containerParentClass);
343
344 11
        if ($this->hasPrivateServices > 0) {
345 11
            $compiledContainerNode
346 11
                ->addStmt($this->builder->property('privates')->makeProtected()->setType('array')->makeStatic())
347 11
                ->addStmt($this->builder->method('__construct')->makePublic()
348 11
                    ->addStmt($this->builder->staticCall($this->builder->constFetch('parent'), '__construct'))
349 11
                    ->addStmt(new Assign(new StaticPropertyFetch(new Name('self'), 'privates'), $this->builder->val([]))))
350 11
            ;
351 11
        }
352 11
353 11
        return $compiledContainerNode
354 11
            ->setDocComment(Builder\CodePrinter::COMMENT)
355 11
            ->addStmts($serviceMethods)
356 11
            ->addStmt($this->builder->property('parameters')
357 11
                ->makePublic()->setType('array')
358
                ->setDefault($parameters))
359
            ->addStmt($this->builder->property('methodsMap')
360
                ->makeProtected()->setType('array')
361
                ->setDefault($methodsMap))
362
            ->addStmt($this->builder->property('types')
363
                ->makeProtected()->setType('array')
364
                ->setDefault($wiredTypes))
365
            ->addStmt($this->builder->property('aliases')
366 11
                ->makeProtected()->setType('array')
367
                ->setDefault($this->aliases))
368 11
        ;
369 11
    }
370
371 11
    /**
372 10
     * Analyse all definitions, build definitions and return results.
373
     *
374 10
     * @param Definition[]|RawDefinition[] $definitions
375 3
     */
376
    protected function doAnalyse(array $definitions): array
377 3
    {
378
        $methodsMap = $serviceMethods = $wiredTypes = [];
379
        \ksort($definitions);
380 9
381
        foreach ($definitions as $id => $definition) {
382
            $serviceMethods[] = $this->doCreate($id, $definition, true);
383
384 11
            if ($this->ignoredDefinition($definition)) {
385 2
                ++$this->hasPrivateServices;
386 1
387
                continue;
388
            }
389
390
            $methodsMap[$id] = Definition::createMethod($id);
391 11
        }
392 11
393 1
        // Remove private aliases
394
        foreach ($this->aliases as $aliased => $service) {
395
            if ($this->ignoredDefinition($definitions[$service] ?? null)) {
396 11
                unset($this->aliases[$aliased]);
397 11
            }
398
        }
399 11
400
        // Prevent autowired private services from be exported.
401
        foreach ($this->types as $type => $ids) {
402 11
            if (1 === \count($ids) && $this->ignoredDefinition($definitions[\reset($ids)] ?? null)) {
403
                continue;
404
            }
405
406
            $ids = \array_filter($ids, fn (string $id): bool => !$this->ignoredDefinition($definitions[$id] ?? null));
407
            $ids = \array_values($ids); // If $ids are filtered, keys should not be preserved.
408 11
409
            $wiredTypes[] = new ArrayItem($this->builder->val($ids), $this->builder->constFetch($type . '::class'));
410 11
        }
411
412
        return [$methodsMap, $serviceMethods, $wiredTypes];
413
    }
414
415
    /**
416
     * @param RawDefinition|Definition|null $def
417
     */
418
    private function ignoredDefinition($def): bool
419
    {
420
        return $def instanceof Definition && !$def->isPublic();
421
    }
422
}
423