Registry::entries()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 10
cc 3
nc 2
nop 1
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Chuck;
6
7
use Closure;
8
use Conia\Chuck\Di\Entry;
9
use Conia\Chuck\Di\Resolver;
10
use Conia\Chuck\Exception\NotFoundException;
11
use Psr\Container\ContainerInterface as PsrContainer;
12
13
/**
14
 * @psalm-api
15
 *
16
 * @psalm-type EntryArray = array<never, never>|array<string, Entry>
17
 */
18
class Registry implements PsrContainer
19
{
20
    protected Resolver $resolver;
21
22
    /** @psalm-var EntryArray */
23
    protected array $entries = [];
24
25
    /** @psalm-var array<never, never>|array<non-empty-string, self> */
26
    protected array $tags = [];
27
28 160
    public function __construct(
29
        protected readonly ?PsrContainer $container = null,
30
        public readonly bool $autowire = true,
31
        protected readonly string $tag = '',
32
        protected readonly ?Registry $parent = null,
33
    ) {
34 160
        if ($container) {
35 2
            $this->add(PsrContainer::class, $container);
36 2
            $this->add($container::class, $container);
37
        } else {
38 160
            $this->add(PsrContainer::class, $this);
39
        }
40
41 160
        $this->add(Registry::class, $this);
42 160
        $this->resolver = new Resolver($this);
43
    }
44
45 6
    public function has(string $id): bool
46
    {
47 6
        return isset($this->entries[$id]) || $this->container?->has($id);
48
    }
49
50
    /** @psalm-return list<string> */
51 6
    public function entries(bool $includeRegistry = false): array
52
    {
53 6
        $keys = array_keys($this->entries);
54
55 6
        if ($includeRegistry) {
56 1
            return $keys;
57
        }
58
59 6
        return array_values(array_filter($keys, function ($item) {
60 6
            return $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class);
61 6
        }));
62
    }
63
64 4
    public function entry(string $id): Entry
65
    {
66 4
        return $this->entries[$id];
67
    }
68
69 117
    public function get(string $id): mixed
70
    {
71 117
        $entry = $this->entries[$id] ?? null;
72
73 117
        if ($entry) {
74 102
            return $this->resolveEntry($entry);
75
        }
76
77 50
        if ($this->container && $this->container->has($id)) {
78 2
            return $this->container->get($id);
79
        }
80
81
        // We are in a tag. Unregistered entries should always
82
        // be registered on the root.
83 48
        if ($this->parent) {
84 29
            return $this->parent->get($id);
85
        }
86
87
        // Autowiring: $id does not exists as an entry in the registry
88 21
        if ($this->autowire && class_exists($id)) {
89 17
            return $this->resolver->autowire($id);
90
        }
91
92 10
        throw new NotFoundException('Unresolvable id: ' . $id);
93
    }
94
95
    /**
96
     * @psalm-param non-empty-string $id
97
     */
98 160
    public function add(
99
        string $id,
100
        mixed $value = null,
101
    ): Entry {
102 160
        $entry = new Entry($id, $value ?? $id);
103 160
        $this->entries[$id] = $entry;
104
105 160
        return $entry;
106
    }
107
108
    /** @psalm-param non-empty-string $tag */
109 123
    public function tag(string $tag): Registry
110
    {
111 123
        if (!isset($this->tags[$tag])) {
112 123
            $this->tags[$tag] = new self(tag: $tag, parent: $this);
113
        }
114
115 123
        return $this->tags[$tag];
116
    }
117
118 6
    public function new(string $id, mixed ...$args): object
119
    {
120 6
        $entry = $this->entries[$id] ?? null;
121
122 6
        if ($entry) {
123
            /** @var mixed */
124 3
            $value = $entry->definition();
125
126 3
            if (is_string($value)) {
127 3
                if (interface_exists($value)) {
128 1
                    return $this->new($value, ...$args);
129
                }
130
131 3
                if (class_exists($value)) {
132
                    /** @psalm-suppress MixedMethodCall */
133 3
                    return new $value(...$args);
134
                }
135
            }
136
        }
137
138 3
        if (class_exists($id)) {
139
            /** @psalm-suppress MixedMethodCall */
140 2
            return new $id(...$args);
141
        }
142
143 1
        throw new NotFoundException('Cannot instantiate ' . $id);
144
    }
145
146 94
    protected function callAndReify(Entry $entry, mixed $value): mixed
147
    {
148 94
        foreach ($entry->getCalls() as $call) {
149 1
            $methodToResolve = $call->method;
150
151
            /** @psalm-var callable */
152 1
            $callable = [$value, $methodToResolve];
153 1
            $args = $this->resolver->resolveCallableArgs($callable, $call->args);
154 1
            $callable(...$args);
155
        }
156
157 94
        if ($entry->shouldReify()) {
158 93
            $entry->set($value);
159
        }
160
161 94
        return $value;
162
    }
163
164 102
    protected function resolveEntry(Entry $entry): mixed
165
    {
166 102
        if ($entry->shouldReturnAsIs()) {
167 2
            return $entry->definition();
168
        }
169
170
        /** @var mixed - the current value, instantiated or definition */
171 101
        $value = $entry->get();
172
173 101
        if (is_string($value)) {
174 91
            if (class_exists($value)) {
175 90
                $constructor = $entry->getConstructor();
176 90
                $args = $entry->getArgs();
177
178 90
                if (isset($args)) {
179
                    // Don't autowire if $args are given
180 17
                    if ($args instanceof Closure) {
181
                        /** @psalm-var array<string, mixed> */
182 3
                        $args = $args(...$this->resolver->resolveCallableArgs($args));
183
184 3
                        return $this->callAndReify(
185 3
                            $entry,
186 3
                            $this->resolver->autowire($value, $args)
187 3
                        );
188
                    }
189
190 14
                    return $this->callAndReify(
191 14
                        $entry,
192 14
                        $this->resolver->autowire($value, $args, $constructor)
193 14
                    );
194
                }
195
196 84
                return $this->callAndReify(
197 84
                    $entry,
198 84
                    $this->resolver->autowire($value, [], $constructor)
199 84
                );
200
            }
201
202 2
            if (isset($this->entries[$value])) {
203 1
                return $this->get($value);
204
            }
205
        }
206
207 43
        if ($value instanceof Closure) {
208 6
            $args = $entry->getArgs();
209
210 6
            if (is_null($args)) {
211 4
                $args = $this->resolver->resolveCallableArgs($value);
212 2
            } elseif ($args instanceof Closure) {
213
                /** @var array<string, mixed> */
214 1
                $args = $args();
215
            }
216
217
            /** @var mixed */
218 6
            $result = $value(...$args);
219
220 6
            return $this->callAndReify($entry, $result);
221
        }
222
223 39
        if (is_object($value)) {
224 38
            return $value;
225
        }
226
227 1
        throw new NotFoundException('Unresolvable id: ' . (string)$value);
228
    }
229
}
230