Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
202 / 202
96.08% covered (success)
96.08%
147 / 153
47.97% covered (danger)
47.97%
71 / 148
83.33% covered (warning)
83.33%
20 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
Container
100.00% covered (success)
100.00%
202 / 202
96.08% covered (success)
96.08%
147 / 153
47.97% covered (danger)
47.97%
71 / 148
100.00% covered (success)
100.00%
24 / 24
981.30
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 scope
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 reset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 has
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 entries
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 entry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
30 / 30
95.24% covered (success)
95.24%
20 / 21
66.67% covered (warning)
66.67%
8 / 12
100.00% covered (success)
100.00%
1 / 1
12.00
 definition
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 add
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addEntry
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tag
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
9 / 9
57.14% covered (warning)
57.14%
4 / 7
100.00% covered (success)
100.00%
1 / 1
6.97
 new
100.00% covered (success)
100.00%
11 / 11
86.67% covered (warning)
86.67%
13 / 15
21.05% covered (danger)
21.05%
4 / 19
100.00% covered (success)
100.00%
1 / 1
23.71
 resolveEntry
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
9 / 9
57.14% covered (warning)
57.14%
4 / 7
100.00% covered (success)
100.00%
1 / 1
6.97
 resolutionContainers
100.00% covered (success)
100.00%
5 / 5
77.78% covered (warning)
77.78%
7 / 9
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
4.25
 materialize
100.00% covered (success)
100.00%
35 / 35
95.24% covered (success)
95.24%
20 / 21
39.13% covered (danger)
39.13%
9 / 23
100.00% covered (success)
100.00%
1 / 1
32.55
 applyCalls
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
4 / 4
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 resetScope
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
12 / 12
13.89% covered (danger)
13.89%
5 / 36
100.00% covered (success)
100.00%
1 / 1
20.96
 resetIfNeeded
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 trackAndReturn
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 findEntry
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 root
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 assertMutable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 seal
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
4 / 4
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 isRoot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
5 / 5
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Container;
6
7use Celemas\Container\Exception\ContainerException;
8use Celemas\Container\Exception\NotFoundException;
9use Celemas\Wire\CallableResolver;
10use Celemas\Wire\Creator;
11use Celemas\Wire\Exception\WireException;
12use Celemas\Wire\WireContainer;
13use Closure;
14use Override;
15use Psr\Container\ContainerExceptionInterface;
16use Psr\Container\ContainerInterface as PsrContainer;
17use Throwable;
18
19/** @psalm-api */
20class Container implements WireContainer
21{
22    protected Creator $creator;
23    protected readonly ?PsrContainer $wrappedContainer;
24    protected bool $sealed = false;
25
26    /** @var array<string, Entry> */
27    protected array $entries = [];
28
29    /** @var array<string, mixed> */
30    protected array $instances = [];
31
32    /** @var array<non-empty-string, self> */
33    protected array $tags = [];
34
35    /** @var array<int, Resettable> */
36    protected array $usedResettables = [];
37
38    public function __construct(
39        public readonly bool $autowire = true,
40        ?PsrContainer $container = null,
41        protected readonly string $tag = '',
42        protected readonly ?Container $parent = null,
43        protected readonly bool $isScope = false,
44    ) {
45        if ($container) {
46            $this->wrappedContainer = $container;
47            $this->add(PsrContainer::class, $container);
48            $this->add($container::class, $container);
49        } else {
50            $this->wrappedContainer = null;
51            $this->add(PsrContainer::class, $this);
52        }
53        $this->add(Container::class, $this);
54        $this->creator = new Creator($this);
55    }
56
57    public function scope(): Container
58    {
59        $root = $this->root();
60
61        if (!$root->sealed) {
62            $root->seal();
63        }
64
65        return new self(
66            autowire: $root->autowire,
67            parent: $root,
68            isScope: true,
69        );
70    }
71
72    public function reset(): void
73    {
74        if (!$this->isScope) {
75            return;
76        }
77
78        $resetIds = [];
79        $this->resetScope($resetIds);
80    }
81
82    #[Override]
83    public function has(string $id): bool
84    {
85        return (
86            isset($this->entries[$id])
87            || $this->parent?->has($id)
88            || $this->wrappedContainer?->has($id)
89        );
90    }
91
92    /** @return list<string> */
93    public function entries(bool $includeContainer = false): array
94    {
95        $keys = array_keys($this->entries);
96
97        if ($includeContainer) {
98            return $keys;
99        }
100
101        return array_values(array_filter(
102            $keys,
103            static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class),
104        ));
105    }
106
107    public function entry(string $id): Entry
108    {
109        return $this->entries[$id];
110    }
111
112    #[Override]
113    public function get(string $id): mixed
114    {
115        try {
116            if (array_key_exists($id, $this->instances)) {
117                return $this->trackAndReturn($this->instances[$id]);
118            }
119
120            $resolved = $this->findEntry($id);
121
122            if ($resolved !== null) {
123                return $this->trackAndReturn(
124                    $this->resolveEntry(
125                        entryOwner: $resolved[0],
126                        entry: $resolved[1],
127                        id: $id,
128                        requester: $this,
129                    ),
130                );
131            }
132
133            $wrappedContainer = $this->root()->wrappedContainer;
134
135            if ($wrappedContainer?->has($id)) {
136                return $this->trackAndReturn($wrappedContainer->get($id));
137            }
138
139            // Autowiring: $id does not exists as an entry in the container
140            if ($this->autowire && class_exists($id)) {
141                return $this->trackAndReturn($this->creator->create($id));
142            }
143        } catch (WireException $e) {
144            throw new NotFoundException(
145                'Unresolvable id: ' . $id . ' - Details: ' . $e->getMessage(),
146                previous: $e,
147            );
148        } catch (ContainerExceptionInterface $e) {
149            throw $e;
150        } catch (Throwable $e) {
151            throw new ContainerException(
152                'Unresolvable id: ' . $id . ' - ' . $e->getMessage(),
153                previous: $e,
154            );
155        }
156
157        throw new NotFoundException('Unresolvable id: ' . $id);
158    }
159
160    #[Override]
161    public function definition(string $id): mixed
162    {
163        $resolved = $this->findEntry($id);
164        $entry = $resolved[1] ?? null;
165
166        if ($entry !== null) {
167            return $entry->definition();
168        }
169
170        throw new NotFoundException('Unresolvable definition - id: ' . $id);
171    }
172
173    /**
174     * @param non-empty-string $id
175     */
176    public function add(
177        string $id,
178        mixed $value = null,
179    ): Entry {
180        $this->assertMutable();
181        $entry = new Entry($id, $value ?? $id);
182        $this->entries[$id] = $entry;
183        unset($this->instances[$id]);
184
185        return $entry;
186    }
187
188    public function addEntry(
189        Entry $entry,
190    ): Entry {
191        $this->assertMutable();
192        $this->entries[$entry->id] = $entry;
193        unset($this->instances[$entry->id]);
194
195        return $entry;
196    }
197
198    /** @param non-empty-string $tag */
199    public function tag(string $tag): Container
200    {
201        if (isset($this->tags[$tag])) {
202            return $this->tags[$tag];
203        }
204
205        if ($this->isRoot() && $this->sealed) {
206            throw new ContainerException('The root container is sealed after scope() was called');
207        }
208
209        $parent = $this;
210        $isScope = false;
211
212        if ($this->isScope) {
213            $root = $this->root();
214            $parent = $root->tags[$tag] ?? $root;
215            $isScope = true;
216        }
217
218        $this->tags[$tag] = new self(
219            autowire: $this->autowire,
220            tag: $tag,
221            parent: $parent,
222            isScope: $isScope,
223        );
224
225        return $this->tags[$tag];
226    }
227
228    public function new(string $id, mixed ...$args): object
229    {
230        $entry = $this->entries[$id] ?? null;
231
232        if ($entry) {
233            /** @var mixed $value */
234            $value = $entry->definition();
235
236            if (is_string($value)) {
237                if (interface_exists($value)) {
238                    return $this->new($value, ...$args);
239                }
240
241                if (class_exists($value)) {
242                    /** @psalm-suppress MixedMethodCall */
243                    return new $value(...$args);
244                }
245            }
246        }
247
248        if (class_exists($id)) {
249            /** @psalm-suppress MixedMethodCall */
250            return new $id(...$args);
251        }
252
253        throw new NotFoundException('Cannot instantiate ' . $id);
254    }
255
256    protected function resolveEntry(
257        Container $entryOwner,
258        Entry $entry,
259        string $id,
260        Container $requester,
261    ): mixed {
262        if ($entry->shouldReturnValue()) {
263            return $entry->definition();
264        }
265
266        [$cacheContainer, $resolutionContext] = $this->resolutionContainers(
267            $entry,
268            $entryOwner,
269            $requester,
270        );
271
272        if ($cacheContainer !== null && array_key_exists($id, $cacheContainer->instances)) {
273            return $cacheContainer->instances[$id];
274        }
275
276        // materialize() intentionally returns mixed because container entries may resolve to any value type.
277        /** @psalm-suppress MixedAssignment */
278        $result = $this->materialize($entry, $resolutionContext);
279
280        if ($cacheContainer !== null) {
281            $cacheContainer->instances[$id] = $result;
282        }
283
284        return $result;
285    }
286
287    /**
288     * @return array{0: null|Container, 1: Container}
289     */
290    protected function resolutionContainers(
291        Entry $entry,
292        Container $entryOwner,
293        Container $requester,
294    ): array {
295        return match ($entry->getLifetime()) {
296            Lifetime::Shared => [$entryOwner, $entryOwner],
297            Lifetime::Scoped => [$requester, $requester],
298            Lifetime::Transient => [null, $requester],
299        };
300    }
301
302    protected function materialize(Entry $entry, Container $context): mixed
303    {
304        /** @var mixed $value */
305        $value = $entry->definition();
306
307        if (is_string($value)) {
308            if (class_exists($value)) {
309                $constructor = $entry->getConstructor();
310                $args = $entry->getArgs();
311
312                if (isset($args)) {
313                    // Don't autowire if $args are given
314                    if ($args instanceof Closure) {
315                        $args = $args(...new CallableResolver($context->creator)->resolve($args));
316
317                        return $this->applyCalls($entry, $context->creator->create($value, $args), $context);
318                    }
319
320                    return $this->applyCalls(
321                        $entry,
322                        $context->creator->create(
323                            $value,
324                            predefinedArgs: $args,
325                            constructor: $constructor ?? '',
326                        ),
327                        $context,
328                    );
329                }
330
331                return $this->applyCalls(
332                    $entry,
333                    $context->creator->create($value, constructor: $constructor ?? ''),
334                    $context,
335                );
336            }
337
338            if ($context->has($value)) {
339                return $context->get($value);
340            }
341        }
342
343        if ($value instanceof Closure) {
344            $args = $entry->getArgs();
345
346            if (is_null($args)) {
347                $args = new CallableResolver($context->creator)->resolve($value);
348            } elseif ($args instanceof Closure) {
349                $args = $args();
350            }
351
352            return $this->applyCalls($entry, $value(...$args), $context);
353        }
354
355        if (is_object($value)) {
356            return $value;
357        }
358
359        throw new NotFoundException('Unresolvable id: ' . (string) $value);
360    }
361
362    protected function applyCalls(Entry $entry, mixed $value, Container $context): mixed
363    {
364        foreach ($entry->getCalls() as $call) {
365            $methodToResolve = $call->method;
366
367            /** @var callable */
368            $callable = [$value, $methodToResolve];
369            $args = new CallableResolver($context->creator)->resolve($callable, $call->args);
370            $callable(...$args);
371        }
372
373        return $value;
374    }
375
376    /**
377     * @param array<int, true> $resetIds
378     */
379    protected function resetScope(array &$resetIds): void
380    {
381        // $instances stores mixed values by design, so foreach assigns mixed to $instance.
382        /** @psalm-suppress MixedAssignment */
383        foreach ($this->instances as $instance) {
384            $this->resetIfNeeded($instance, $resetIds);
385        }
386
387        foreach ($this->usedResettables as $usedResettable) {
388            $this->resetIfNeeded($usedResettable, $resetIds);
389        }
390
391        foreach ($this->tags as $tagContainer) {
392            if (!$tagContainer->isScope) {
393                continue;
394            }
395
396            $tagContainer->resetScope($resetIds);
397        }
398
399        $this->instances = [];
400        $this->entries = [];
401        $this->tags = [];
402        $this->usedResettables = [];
403        $this->add(PsrContainer::class, $this);
404        $this->add(Container::class, $this);
405    }
406
407    /**
408     * @param array<int, true> $resetIds
409     */
410    protected function resetIfNeeded(mixed $value, array &$resetIds): void
411    {
412        if (!$value instanceof Resettable) {
413            return;
414        }
415
416        $objectId = spl_object_id($value);
417
418        if (isset($resetIds[$objectId])) {
419            return;
420        }
421
422        $resetIds[$objectId] = true;
423        $value->reset();
424    }
425
426    protected function trackAndReturn(mixed $value): mixed
427    {
428        if ($this->isScope && $value instanceof Resettable) {
429            $this->usedResettables[spl_object_id($value)] = $value;
430        }
431
432        return $value;
433    }
434
435    /** @return array{Container, Entry}|null */
436    protected function findEntry(string $id): ?array
437    {
438        $entry = $this->entries[$id] ?? null;
439
440        if ($entry !== null) {
441            return [$this, $entry];
442        }
443
444        return $this->parent?->findEntry($id);
445    }
446
447    protected function root(): Container
448    {
449        $container = $this;
450
451        while ($container->parent !== null) {
452            $container = $container->parent;
453        }
454
455        return $container;
456    }
457
458    protected function assertMutable(): void
459    {
460        if ($this->sealed) {
461            throw new ContainerException('The root container is sealed after scope() was called');
462        }
463    }
464
465    protected function seal(): void
466    {
467        $this->sealed = true;
468
469        foreach ($this->tags as $tagContainer) {
470            $tagContainer->seal();
471        }
472    }
473
474    protected function isRoot(): bool
475    {
476        return $this->parent === null && $this->tag === '' && !$this->isScope;
477    }
478}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Container->__construct
39        public readonly bool $autowire = true,
40        ?PsrContainer $container = null,
41        protected readonly string $tag = '',
42        protected readonly ?Container $parent = null,
43        protected readonly bool $isScope = false,
44    ) {
45        if ($container) {
45        if ($container) {
46            $this->wrappedContainer = $container;
50            $this->wrappedContainer = null;
51            $this->add(PsrContainer::class, $this);
52        }
53        $this->add(Container::class, $this);
53        $this->add(Container::class, $this);
54        $this->creator = new Creator($this);
55    }
Container->add
177        string $id,
178        mixed $value = null,
179    ): Entry {
180        $this->assertMutable();
181        $entry = new Entry($id, $value ?? $id);
182        $this->entries[$id] = $entry;
183        unset($this->instances[$id]);
184
185        return $entry;
186    }
Container->addEntry
189        Entry $entry,
190    ): Entry {
191        $this->assertMutable();
192        $this->entries[$entry->id] = $entry;
193        unset($this->instances[$entry->id]);
194
195        return $entry;
196    }
Container->applyCalls
362    protected function applyCalls(Entry $entry, mixed $value, Container $context): mixed
363    {
364        foreach ($entry->getCalls() as $call) {
364        foreach ($entry->getCalls() as $call) {
364        foreach ($entry->getCalls() as $call) {
365            $methodToResolve = $call->method;
364        foreach ($entry->getCalls() as $call) {
365            $methodToResolve = $call->method;
366
367            /** @var callable */
368            $callable = [$value, $methodToResolve];
369            $args = new CallableResolver($context->creator)->resolve($callable, $call->args);
370            $callable(...$args);
371        }
372
373        return $value;
374    }
Container->assertMutable
460        if ($this->sealed) {
461            throw new ContainerException('The root container is sealed after scope() was called');
463    }
Container->definition
161    public function definition(string $id): mixed
162    {
163        $resolved = $this->findEntry($id);
164        $entry = $resolved[1] ?? null;
165
166        if ($entry !== null) {
167            return $entry->definition();
170        throw new NotFoundException('Unresolvable definition - id: ' . $id);
171    }
Container->entries
93    public function entries(bool $includeContainer = false): array
94    {
95        $keys = array_keys($this->entries);
96
97        if ($includeContainer) {
98            return $keys;
101        return array_values(array_filter(
102            $keys,
103            static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class),
104        ));
105    }
Container->entry
107    public function entry(string $id): Entry
108    {
109        return $this->entries[$id];
110    }
Container->findEntry
436    protected function findEntry(string $id): ?array
437    {
438        $entry = $this->entries[$id] ?? null;
439
440        if ($entry !== null) {
441            return [$this, $entry];
444        return $this->parent?->findEntry($id);
445    }
Container->get
113    public function get(string $id): mixed
114    {
115        try {
116            if (array_key_exists($id, $this->instances)) {
117                return $this->trackAndReturn($this->instances[$id]);
120            $resolved = $this->findEntry($id);
121
122            if ($resolved !== null) {
123                return $this->trackAndReturn(
124                    $this->resolveEntry(
125                        entryOwner: $resolved[0],
133            $wrappedContainer = $this->root()->wrappedContainer;
134
135            if ($wrappedContainer?->has($id)) {
136                return $this->trackAndReturn($wrappedContainer->get($id));
140            if ($this->autowire && class_exists($id)) {
140            if ($this->autowire && class_exists($id)) {
140            if ($this->autowire && class_exists($id)) {
140            if ($this->autowire && class_exists($id)) {
140            if ($this->autowire && class_exists($id)) {
140            if ($this->autowire && class_exists($id)) {
141                return $this->trackAndReturn($this->creator->create($id));
141                return $this->trackAndReturn($this->creator->create($id));
143        } catch (WireException $e) {
144            throw new NotFoundException(
145                'Unresolvable id: ' . $id . ' - Details: ' . $e->getMessage(),
148        } catch (ContainerExceptionInterface $e) {
149            throw $e;
150        } catch (Throwable $e) {
151            throw new ContainerException(
152                'Unresolvable id: ' . $id . ' - ' . $e->getMessage(),
157        throw new NotFoundException('Unresolvable id: ' . $id);
158    }
Container->has
83    public function has(string $id): bool
84    {
85        return (
86            isset($this->entries[$id])
87            || $this->parent?->has($id)
87            || $this->parent?->has($id)
88            || $this->wrappedContainer?->has($id)
88            || $this->wrappedContainer?->has($id)
89        );
90    }
Container->isRoot
476        return $this->parent === null && $this->tag === '' && !$this->isScope;
476        return $this->parent === null && $this->tag === '' && !$this->isScope;
476        return $this->parent === null && $this->tag === '' && !$this->isScope;
476        return $this->parent === null && $this->tag === '' && !$this->isScope;
476        return $this->parent === null && $this->tag === '' && !$this->isScope;
477    }
Container->materialize
302    protected function materialize(Entry $entry, Container $context): mixed
303    {
304        /** @var mixed $value */
305        $value = $entry->definition();
306
307        if (is_string($value)) {
308            if (class_exists($value)) {
308            if (class_exists($value)) {
308            if (class_exists($value)) {
308            if (class_exists($value)) {
309                $constructor = $entry->getConstructor();
310                $args = $entry->getArgs();
311
312                if (isset($args)) {
314                    if ($args instanceof Closure) {
315                        $args = $args(...new CallableResolver($context->creator)->resolve($args));
316
317                        return $this->applyCalls($entry, $context->creator->create($value, $args), $context);
320                    return $this->applyCalls(
321                        $entry,
331                return $this->applyCalls(
332                    $entry,
338            if ($context->has($value)) {
339                return $context->get($value);
343        if ($value instanceof Closure) {
344            $args = $entry->getArgs();
345
346            if (is_null($args)) {
346            if (is_null($args)) {
347                $args = new CallableResolver($context->creator)->resolve($value);
348            } elseif ($args instanceof Closure) {
349                $args = $args();
350            }
351
352            return $this->applyCalls($entry, $value(...$args), $context);
352            return $this->applyCalls($entry, $value(...$args), $context);
355        if (is_object($value)) {
356            return $value;
359        throw new NotFoundException('Unresolvable id: ' . (string) $value);
360    }
Container->new
228    public function new(string $id, mixed ...$args): object
229    {
230        $entry = $this->entries[$id] ?? null;
231
232        if ($entry) {
234            $value = $entry->definition();
235
236            if (is_string($value)) {
237                if (interface_exists($value)) {
238                    return $this->new($value, ...$args);
241                if (class_exists($value)) {
241                if (class_exists($value)) {
241                if (class_exists($value)) {
241                if (class_exists($value)) {
243                    return new $value(...$args);
248        if (class_exists($id)) {
248        if (class_exists($id)) {
248        if (class_exists($id)) {
248        if (class_exists($id)) {
250            return new $id(...$args);
253        throw new NotFoundException('Cannot instantiate ' . $id);
254    }
Container->reset
74        if (!$this->isScope) {
75            return;
78        $resetIds = [];
79        $this->resetScope($resetIds);
80    }
Container->resetIfNeeded
410    protected function resetIfNeeded(mixed $value, array &$resetIds): void
411    {
412        if (!$value instanceof Resettable) {
413            return;
416        $objectId = spl_object_id($value);
417
418        if (isset($resetIds[$objectId])) {
419            return;
422        $resetIds[$objectId] = true;
423        $value->reset();
424    }
Container->resetScope
379    protected function resetScope(array &$resetIds): void
380    {
381        // $instances stores mixed values by design, so foreach assigns mixed to $instance.
382        /** @psalm-suppress MixedAssignment */
383        foreach ($this->instances as $instance) {
383        foreach ($this->instances as $instance) {
383        foreach ($this->instances as $instance) {
384            $this->resetIfNeeded($instance, $resetIds);
383        foreach ($this->instances as $instance) {
384            $this->resetIfNeeded($instance, $resetIds);
385        }
386
387        foreach ($this->usedResettables as $usedResettable) {
387        foreach ($this->usedResettables as $usedResettable) {
387        foreach ($this->usedResettables as $usedResettable) {
388            $this->resetIfNeeded($usedResettable, $resetIds);
387        foreach ($this->usedResettables as $usedResettable) {
388            $this->resetIfNeeded($usedResettable, $resetIds);
389        }
390
391        foreach ($this->tags as $tagContainer) {
391        foreach ($this->tags as $tagContainer) {
392            if (!$tagContainer->isScope) {
393                continue;
391        foreach ($this->tags as $tagContainer) {
392            if (!$tagContainer->isScope) {
393                continue;
394            }
395
396            $tagContainer->resetScope($resetIds);
391        foreach ($this->tags as $tagContainer) {
392            if (!$tagContainer->isScope) {
393                continue;
394            }
395
396            $tagContainer->resetScope($resetIds);
397        }
398
399        $this->instances = [];
400        $this->entries = [];
401        $this->tags = [];
402        $this->usedResettables = [];
403        $this->add(PsrContainer::class, $this);
404        $this->add(Container::class, $this);
405    }
Container->resolutionContainers
291        Entry $entry,
292        Container $entryOwner,
293        Container $requester,
294    ): array {
295        return match ($entry->getLifetime()) {
296            Lifetime::Shared => [$entryOwner, $entryOwner],
297            Lifetime::Scoped => [$requester, $requester],
298            Lifetime::Transient => [null, $requester],
298            Lifetime::Transient => [null, $requester],
298            Lifetime::Transient => [null, $requester],
296            Lifetime::Shared => [$entryOwner, $entryOwner],
297            Lifetime::Scoped => [$requester, $requester],
298            Lifetime::Transient => [null, $requester],
298            Lifetime::Transient => [null, $requester],
299        };
300    }
Container->resolveEntry
257        Container $entryOwner,
258        Entry $entry,
259        string $id,
260        Container $requester,
261    ): mixed {
262        if ($entry->shouldReturnValue()) {
263            return $entry->definition();
266        [$cacheContainer, $resolutionContext] = $this->resolutionContainers(
267            $entry,
268            $entryOwner,
269            $requester,
270        );
271
272        if ($cacheContainer !== null && array_key_exists($id, $cacheContainer->instances)) {
272        if ($cacheContainer !== null && array_key_exists($id, $cacheContainer->instances)) {
272        if ($cacheContainer !== null && array_key_exists($id, $cacheContainer->instances)) {
273            return $cacheContainer->instances[$id];
278        $result = $this->materialize($entry, $resolutionContext);
279
280        if ($cacheContainer !== null) {
281            $cacheContainer->instances[$id] = $result;
282        }
283
284        return $result;
284        return $result;
285    }
Container->root
449        $container = $this;
450
451        while ($container->parent !== null) {
451        while ($container->parent !== null) {
452            $container = $container->parent;
451        while ($container->parent !== null) {
455        return $container;
456    }
Container->scope
59        $root = $this->root();
60
61        if (!$root->sealed) {
62            $root->seal();
63        }
64
65        return new self(
65        return new self(
66            autowire: $root->autowire,
67            parent: $root,
68            isScope: true,
69        );
70    }
Container->seal
467        $this->sealed = true;
468
469        foreach ($this->tags as $tagContainer) {
469        foreach ($this->tags as $tagContainer) {
469        foreach ($this->tags as $tagContainer) {
470            $tagContainer->seal();
469        foreach ($this->tags as $tagContainer) {
470            $tagContainer->seal();
471        }
472    }
Container->tag
199    public function tag(string $tag): Container
200    {
201        if (isset($this->tags[$tag])) {
202            return $this->tags[$tag];
205        if ($this->isRoot() && $this->sealed) {
205        if ($this->isRoot() && $this->sealed) {
205        if ($this->isRoot() && $this->sealed) {
206            throw new ContainerException('The root container is sealed after scope() was called');
209        $parent = $this;
210        $isScope = false;
211
212        if ($this->isScope) {
213            $root = $this->root();
214            $parent = $root->tags[$tag] ?? $root;
215            $isScope = true;
216        }
217
218        $this->tags[$tag] = new self(
218        $this->tags[$tag] = new self(
219            autowire: $this->autowire,
220            tag: $tag,
221            parent: $parent,
222            isScope: $isScope,
223        );
224
225        return $this->tags[$tag];
226    }
Container->trackAndReturn
426    protected function trackAndReturn(mixed $value): mixed
427    {
428        if ($this->isScope && $value instanceof Resettable) {
428        if ($this->isScope && $value instanceof Resettable) {
428        if ($this->isScope && $value instanceof Resettable) {
429            $this->usedResettables[spl_object_id($value)] = $value;
430        }
431
432        return $value;
432        return $value;
433    }
{closure:/workspace/celemas/container/src/Container.php:103-103}
103            static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class),
103            static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class),
103            static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class),