Passed
Push — master ( 6a389c...42f43f )
by Divine Niiquaye
01:03 queued 12s
created

ContainerBuilder::remove()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
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
     * Get the builder use to compiler container.
247
     */
248 48
    public function getBuilder(): \PhpParser\BuilderFactory
249
    {
250 48
        return $this->builder;
251
    }
252
253
    /**
254
     * Compiles the container.
255
     * This method main job is to manipulate and optimize the container.
256
     *
257
     * supported $options config (defaults):
258
     * - strictType => true,
259
     * - printToString => true,
260
     * - shortArraySyntax => true,
261
     * - spacingLevel => 8,
262
     * - containerClass => CompiledContainer,
263
     *
264
     * @throws \ReflectionException
265
     *
266
     * @return \PhpParser\Node[]|string
267
     */
268 11
    public function compile(array $options = [])
269
    {
270 11
        $options += ['strictType' => true, 'printToString' => true, 'containerClass' => 'CompiledContainer'];
271 11
        $astNodes = [];
272
273 11
        foreach ($this->providers as $name => $builder) {
274 1
            if ($this->trackResources) {
275 1
                $this->addResource(new ClassExistenceResource($name, false));
276 1
                $this->addResource(new FileExistenceResource($rPath = (new \ReflectionClass($name))->getFileName()));
277 1
                $this->addResource(new FileResource($rPath));
278
            }
279
280 1
            if ($builder instanceof Builder\PrependInterface) {
281 1
                $builder->before($this);
282
            }
283
        }
284
285 11
        if ($options['strictType']) {
286 11
            $astNodes[] = new Declare_([new DeclareDeclare('strict_types', $this->builder->val(1))]);
287
        }
288
289 11
        $parameters = \array_map(fn ($value) => $this->builder->val($value), $this->parameters);
290 11
        $astNodes[] = $this->doCompile($this->definitions, $parameters, $options['containerClass'])->getNode();
291
292 11
        if ($options['printToString']) {
293 11
            return Builder\CodePrinter::print($astNodes, $options);
294
        }
295
296 1
        return $astNodes;
297
    }
298
299
    /**
300
     * {@inheritdoc}
301
     *
302
     * @param Definition|RawDefinition $service
303
     */
304 17
    protected function doCreate(string $id, $service, bool $build = false)
305
    {
306 17
        if (isset($this->loading[$id])) {
307 6
            throw new CircularReferenceException($id, [...\array_keys($this->loading), $id]);
308
        }
309
310
        try {
311 17
            $this->loading[$id] = true;
312
313 17
            if ($service instanceof RawDefinition) {
314 3
                return $build ? $service->build($id, $this->builder) : $this->builder->val($service());
315
            }
316
317
            // Strict circular reference check ...
318 17
            $compiled = $service->build();
319
320 11
            return $build ? $compiled : $service->resolve();
321
        } finally {
322 17
            unset($this->loading[$id]);
323
        }
324
    }
325
326
    /**
327
     * @param Definition[]|RawDefinition[] $definitions
328
     */
329 11
    protected function doCompile(array $definitions, array $parameters, string $containerClass): \PhpParser\Builder\Class_
330
    {
331 11
        [$methodsMap, $serviceMethods, $wiredTypes] = $this->doAnalyse($definitions);
332 11
        $compiledContainerNode = $this->builder->class($containerClass)->extend($this->containerParentClass);
333
334 11
        if ($this->hasPrivateServices > 0) {
335
            $compiledContainerNode
336 3
                ->addStmt($this->builder->property('privates')->makeProtected()->setType('array')->makeStatic())
337 3
                ->addStmt($this->builder->method('__construct')->makePublic()
338 3
                    ->addStmt($this->builder->staticCall($this->builder->constFetch('parent'), '__construct'))
339 3
                    ->addStmt(new Assign(new StaticPropertyFetch(new Name('self'), 'privates'), $this->builder->val([]))))
340
            ;
341
        }
342
343
        return $compiledContainerNode
344 11
            ->setDocComment(Builder\CodePrinter::COMMENT)
345 11
            ->addStmts($serviceMethods)
346 11
            ->addStmt($this->builder->property('parameters')
347 11
                ->makePublic()->setType('array')
348 11
                ->setDefault($parameters))
349 11
            ->addStmt($this->builder->property('methodsMap')
350 11
                ->makeProtected()->setType('array')
351 11
                ->setDefault($methodsMap))
352 11
            ->addStmt($this->builder->property('types')
353 11
                ->makeProtected()->setType('array')
354 11
                ->setDefault($wiredTypes))
355 11
            ->addStmt($this->builder->property('aliases')
356 11
                ->makeProtected()->setType('array')
357 11
                ->setDefault($this->aliases))
358
        ;
359
    }
360
361
    /**
362
     * Analyse all definitions, build definitions and return results.
363
     *
364
     * @param Definition[]|RawDefinition[] $definitions
365
     */
366 11
    protected function doAnalyse(array $definitions): array
367
    {
368 11
        $methodsMap = $serviceMethods = $wiredTypes = [];
369 11
        \ksort($definitions);
370
371 11
        foreach ($definitions as $id => $definition) {
372 10
            $serviceMethods[] = $this->doCreate($id, $definition, true);
373
374 10
            if ($this->ignoredDefinition($definition)) {
375 3
                ++$this->hasPrivateServices;
376
377 3
                continue;
378
            }
379
380 9
            $methodsMap[$id] = Definition::createMethod($id);
381
        }
382
383
        // Remove private aliases
384 11
        foreach ($this->aliases as $aliased => $service) {
385 2
            if ($this->ignoredDefinition($definitions[$service] ?? null)) {
386 1
                unset($this->aliases[$aliased]);
387
            }
388
        }
389
390
        // Prevent autowired private services from be exported.
391 11
        foreach ($this->types as $type => $ids) {
392 11
            if (1 === \count($ids) && $this->ignoredDefinition($definitions[\reset($ids)] ?? null)) {
393 1
                continue;
394
            }
395
396 11
            $ids = \array_filter($ids, fn (string $id): bool => !$this->ignoredDefinition($definitions[$id] ?? null));
397 11
            $ids = \array_values($ids); // If $ids are filtered, keys should not be preserved.
398
399 11
            $wiredTypes[] = new ArrayItem($this->builder->val($ids), $this->builder->constFetch($type . '::class'));
400
        }
401
402 11
        return [$methodsMap, $serviceMethods, $wiredTypes];
403
    }
404
405
    /**
406
     * @param RawDefinition|Definition|null $def
407
     */
408 11
    private function ignoredDefinition($def): bool
409
    {
410 11
        return $def instanceof Definition && !$def->isPublic();
411
    }
412
}
413