Issues (16)

src/ContainerBuilder.php (1 issue)

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