Passed
Push — main ( bab540...b14978 )
by Thomas
02:20
created

Registry::addEntry()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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