Passed
Push — main ( a1a461...2097df )
by Thomas
12:50
created

Registry::get()   B

Complexity

Conditions 9
Paths 6

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 9

Importance

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