Passed
Push — main ( af6a91...92bf86 )
by Thomas
03:24
created

Registry::new()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 26
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

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