Registry   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 40
eloc 97
c 4
b 0
f 0
dl 0
loc 228
ccs 98
cts 98
cp 1
rs 9.2

11 Methods

Rating   Name   Duplication   Size   Complexity  
A addEntry() 0 6 1
A new() 0 26 6
A entry() 0 3 1
A has() 0 3 3
A __construct() 0 16 2
B resolveEntry() 0 68 11
A add() 0 8 1
A entries() 0 10 3
B get() 0 28 7
A tag() 0 7 2
A callAndReify() 0 16 3

How to fix   Complexity   

Complex Class

Complex classes like Registry often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Registry, and based on these observations, apply Extract Interface, too.

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