Passed
Push — main ( d72029...35e57d )
by Thomas
03:10
created

Registry::resolveEntry()   B

Complexity

Conditions 11
Paths 15

Size

Total Lines 51
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 11

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 51
ccs 25
cts 25
cp 1
rs 7.3166
c 0
b 0
f 0
cc 11
nc 15
nop 1
crap 11

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 125
    public function __construct(
25
        protected readonly ?ContainerInterface $container = null,
26
        protected readonly bool $autowire = true,
27
        protected readonly string $tag = '',
28
    ) {
29 125
        if ($container) {
30 2
            $this->add(ContainerInterface::class, $container);
31 2
            $this->add($container::class, $container);
32
        } else {
33 125
            $this->add(ContainerInterface::class, $this);
34
        }
35
36 125
        $this->add(Registry::class, $this);
37 125
        $this->resolver = new Resolver($this);
38
    }
39
40 2
    public function has(string $id): bool
41
    {
42 2
        return isset($this->entries[$id]) || $this->container?->has($id);
43
    }
44
45 17
    public function entry(string $id, string $paramName = ''): Entry
46
    {
47 17
        $paramName = $this->normalizeParameterName($paramName);
48
49 17
        return $this->entries[$id . $paramName];
50
    }
51
52 81
    public function get(string $id): mixed
53
    {
54 81
        $entry = $this->entries[$id] ?? null;
55
56 81
        if ($entry) {
57 73
            return $this->resolveEntry($entry);
58
        }
59
60 13
        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 11
        if ($this->autowire && class_exists($id)) {
66 8
            return $this->resolver->autowire($id);
67
        }
68
69 4
        $message = empty($this->tag) ?
70 3
            'Unresolvable id: ' . $id :
71 1
            'Unresolvable tagged id: ' . $this->tag . '::' . $id;
72
73 4
        throw new NotFoundException($message);
74
    }
75
76
    /**
77
     * @psalm-param non-empty-string $id
78
     */
79 125
    public function add(
80
        string $id,
81
        mixed $value = null,
82
        string $paramName = '',
83
    ): Entry {
84 125
        $paramName = $this->normalizeParameterName($paramName);
85 125
        $entry = new Entry($id, $value ?? $id);
86 125
        $this->entries[$id . $paramName] = $entry;
87
88 125
        return $entry;
89
    }
90
91
    /** @psalm-param non-empty-string $tag */
92 89
    public function tag(string $tag): Registry
93
    {
94 89
        if (!isset($this->tags[$tag])) {
95 89
            $this->tags[$tag] = new self(tag: $tag);
96
        }
97
98 89
        return $this->tags[$tag];
99
    }
100
101 4
    public function new(string $id, mixed ...$args): object
102
    {
103 4
        $entry = $this->entries[$id] ?? null;
104
105 4
        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 2
        if ($this->autowire && class_exists($id)) {
122
            /** @psalm-suppress MixedMethodCall */
123 1
            return new $id(...$args);
124
        }
125
126 1
        throw new NotFoundException('Cannot instantiate ' . $id);
127
    }
128
129 21
    public function getWithParamName(string $id, string $paramName): mixed
130
    {
131 21
        $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 21
        return isset($this->entries[$id . $paramName]) ?
138 2
            $this->resolveEntry($this->entries[$id . $paramName]) :
139 21
            $this->get($id);
140
    }
141
142 65
    protected function reifyAndReturn(Entry $entry, mixed $value): mixed
143
    {
144 65
        if ($entry->shouldReify()) {
145 64
            $entry->set($value);
146
        }
147
148 65
        return $value;
149
    }
150
151 73
    protected function resolveEntry(Entry $entry): mixed
152
    {
153 73
        if ($entry->shouldReturnAsIs()) {
154 3
            return $entry->definition();
155
        }
156
157
        /** @var mixed - the current value, instantiated or definition */
158 71
        $value = $entry->get();
159
160 71
        if (is_string($value)) {
161 63
            if (class_exists($value)) {
162 62
                $args = $entry->getArgs();
163
164 62
                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 58
                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 9
            $args = $entry->getArgs();
183
184 9
            if (is_null($args)) {
185 7
                $args = $this->resolver->resolveClosureArgs($value);
186 2
            } elseif ($args instanceof Closure) {
187
                /** @var array<string, mixed> */
188 1
                $args = $args();
189
            }
190
191
            /** @var mixed */
192 9
            $result = $value(...$args);
193
194 9
            return $this->reifyAndReturn($entry, $result);
195
        }
196
197 53
        if (is_object($value)) {
198 52
            return $value;
199
        }
200
201 1
        throw new NotFoundException('Unresolvable id: ' . print_r($value, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($value, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

201
        throw new NotFoundException('Unresolvable id: ' . /** @scrutinizer ignore-type */ print_r($value, true));
Loading history...
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->resolveClosureArgs($callback);
215
216
        /** @psalm-suppress MixedMethodCall */
217 1
        return new $class(...$callback(...$args));
218
    }
219
220 125
    protected function normalizeParameterName(string $paramName): string
221
    {
222 125
        if (empty($paramName)) {
223 125
            return $paramName;
224
        }
225
226 22
        $paramName = trim($paramName);
227
228 22
        return str_starts_with($paramName, '$') ? $paramName : '$' . $paramName;
229
    }
230
}
231