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