Passed
Push — main ( 92bf86...8347d6 )
by Thomas
02:44
created

Registry::callAndReify()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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