Passed
Push — main ( 438344...ee0fb9 )
by Thomas
02:54
created

Registry   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 330
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 60
eloc 129
c 0
b 0
f 0
dl 0
loc 330
ccs 145
cts 145
cp 1
rs 3.6

22 Methods

Rating   Name   Duplication   Size   Complexity  
A hasTagged() 0 3 1
A taggedEntry() 0 3 1
A add() 0 10 2
B resolveEntry() 0 53 11
A resolveParam() 0 25 5
A fromArgsArray() 0 4 1
A has() 0 3 2
A tag() 0 3 1
A addTagged() 0 6 1
A getTagged() 0 9 2
A get() 0 18 5
A getWithParamName() 0 11 2
A normalizeParameterName() 0 9 3
A fromArgsClosure() 0 7 1
A autowire() 0 11 2
A resolveArgs() 0 12 3
A entry() 0 5 1
A __construct() 0 14 2
A reifyAndReturn() 0 7 2
A getParamInfo() 0 14 4
A addAnyway() 0 10 1
B new() 0 26 7

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\Chuck;
6
7
use Closure;
8
use Conia\Chuck\Exception\ContainerException;
9
use Conia\Chuck\Exception\NotFoundException;
10
use Psr\Container\ContainerInterface;
11
use ReflectionClass;
12
use ReflectionFunction;
13
use ReflectionFunctionAbstract;
14
use ReflectionMethod;
15
use ReflectionNamedType;
16
use ReflectionParameter;
17
use Throwable;
18
19
/**
20
 * @psalm-type EntryArray = array<never, never>|array<string, RegistryEntry>
21
 */
22
class Registry implements ContainerInterface
23
{
24
    /** @var EntryArray */
0 ignored issues
show
Bug introduced by
The type Conia\Chuck\EntryArray was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
    protected array $entries = [];
26
27
    /** @var array<never, never>|array<string, EntryArray> */
28
    protected array $taggedEntries = [];
29
    protected readonly ?ContainerInterface $container;
30
31 120
    public function __construct(
32
        ?ContainerInterface $container = null,
33
        protected readonly bool $autowire = true
34
    ) {
35 120
        $this->container = $container;
0 ignored issues
show
Bug introduced by
The property container is declared read-only in Conia\Chuck\Registry.
Loading history...
36
37 120
        if ($container) {
38 3
            $this->addAnyway(ContainerInterface::class, $container);
39 3
            $this->addAnyway($container::class, $container);
40
        } else {
41 118
            $this->addAnyway(ContainerInterface::class, $this);
42
        }
43
44 120
        $this->addAnyway(Registry::class, $this);
45
    }
46
47 2
    public function has(string $id): bool
48
    {
49 2
        return isset($this->entries[$id]) || $this->container?->has($id);
50
    }
51
52 3
    public function entry(string $id, string $paramName = ''): mixed
53
    {
54 3
        $paramName = $this->normalizeParameterName($paramName);
55
56 3
        return $this->entries[$id . $paramName];
57
    }
58
59 73
    public function get(string $id): mixed
60
    {
61 73
        $entry = $this->entries[$id] ?? null;
62
63 73
        if ($entry) {
64 66
            return $this->resolveEntry($entry);
65
        }
66
67 12
        if ($this->container?->has($id)) {
68 2
            return $this->container->get($id);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

68
            return $this->container->/** @scrutinizer ignore-call */ get($id);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
69
        }
70
71
        // Autowiring: $id does not exists as an entry in the registry
72 10
        if ($this->autowire && class_exists($id)) {
73 8
            return $this->autowire($id);
74
        }
75
76 3
        throw new NotFoundException('Unresolvable id: ' . $id);
77
    }
78
79
    /**
80
     * @param non-empty-string $id
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
81
     */
82 88
    public function add(
83
        string $id,
84
        mixed $value = null,
85
        string $paramName = '',
86
    ): RegistryEntry {
87 88
        if ($this->container) {
88 1
            throw new ContainerException('Third party container implementation in use');
89
        }
90
91 87
        return $this->addAnyway($id, $value, $paramName);
92
    }
93
94
    /**
95
     * @param non-empty-string $id
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
96
     */
97 120
    public function addAnyway(
98
        string $id,
99
        mixed $value = null,
100
        string $paramName = '',
101
    ): RegistryEntry {
102 120
        $paramName = $this->normalizeParameterName($paramName);
103 120
        $entry = new RegistryEntry($id, $value ?? $id);
104 120
        $this->entries[$id . $paramName] = $entry;
105
106 120
        return $entry;
107
    }
108
109 3
    public function tag(string $tag): RegistryTag
110
    {
111 3
        return new RegistryTag($tag, $this);
112
    }
113
114 1
    public function hasTagged(string $tag, string $id): bool
115
    {
116 1
        return isset($this->taggedEntries[$tag][$id]);
117
    }
118
119 2
    public function taggedEntry(string $tag, string $id): RegistryEntry
120
    {
121 2
        return $this->taggedEntries[$tag][$id];
122
    }
123
124 2
    public function getTagged(string $tag, string $id): mixed
125
    {
126 2
        $entry = $this->taggedEntries[$tag][$id] ?? null;
127
128 2
        if ($entry) {
129 1
            return $this->resolveEntry($entry);
130
        }
131
132 1
        throw new NotFoundException('Unresolvable tagged id: ' . $tag . '::' . $id);
133
    }
134
135
    /**
136
     * @param non-empty-string $id
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
137
     */
138 2
    public function addTagged(string $tag, string $id, mixed $value = null): RegistryEntry
139
    {
140 2
        $entry = new RegistryEntry($id, $value ?? $id);
141 2
        $this->taggedEntries[$tag][$id] = $entry;
142
143 2
        return $entry;
144
    }
145
146 4
    public function new(string $id, mixed ...$args): object
147
    {
148 4
        $entry = $this->entries[$id] ?? null;
149
150 4
        if ($entry) {
151
            /** @var mixed */
152 2
            $value = $entry->definition();
153
154 2
            if (is_string($value)) {
155 2
                if (interface_exists($value)) {
156 1
                    return $this->new($value, ...$args);
157
                }
158
159 2
                if (class_exists($value)) {
160
                    /** @psalm-suppress MixedMethodCall */
161 2
                    return new $value(...$args);
162
                }
163
            }
164
        }
165
166 2
        if ($this->autowire && class_exists($id)) {
167
            /** @psalm-suppress MixedMethodCall */
168 1
            return new $id(...$args);
169
        }
170
171 1
        throw new NotFoundException('Cannot instantiate ' . $id);
172
    }
173
174 21
    public function getWithParamName(string $id, string $paramName): mixed
175
    {
176 21
        $paramName = $this->normalizeParameterName($paramName);
177
178
        // See if there's a entry with a bound parameter name:
179
        // e. g. '\Namespace\MyClass$myParameter'
180
        // If $paramName is emtpy an existing unbound entry should
181
        // be found on first try.
182 21
        return isset($this->entries[$id . $paramName]) ?
183 2
            $this->resolveEntry($this->entries[$id . $paramName]) :
184 21
            $this->get($id);
185
    }
186
187 24
    public function resolveParam(ReflectionParameter $param): mixed
188
    {
189 24
        $type = $param->getType();
190
191 24
        if ($type instanceof ReflectionNamedType) {
192
            try {
193 20
                return $this->getWithParamName($type->getName(), '$' . ltrim($param->getName(), '?'));
194 3
            } catch (NotFoundException $e) {
195 2
                if ($param->isDefaultValueAvailable()) {
196 1
                    return $param->getDefaultValue();
197
                }
198
199 1
                throw $e;
200
            }
201
        } else {
202 4
            if ($type) {
203 2
                throw new ContainerException(
204 2
                    "Autowiring does not support union or intersection types. Source: \n" .
205 2
                        $this->getParamInfo($param)
206 2
                );
207
            }
208
209 2
            throw new ContainerException(
210 2
                "Autowired entities need to have typed constructor parameters. Source: \n" .
211 2
                    $this->getParamInfo($param)
212 2
            );
213
        }
214
    }
215
216 6
    public function getParamInfo(ReflectionParameter $param): string
217
    {
218 6
        $type = $param->getType();
219 6
        $rf = $param->getDeclaringFunction();
220 6
        $rc = null;
221
222 6
        if ($rf instanceof ReflectionMethod) {
223 6
            $rc = $rf->getDeclaringClass();
224
        }
225
226 6
        return ($rc ? $rc->getName() . '::' : '') .
227 6
            ($rf->getName() . '(..., ') .
228 6
            ($type ? (string)$type . ' ' : '') .
229 6
            '$' . $param->getName() . ', ...)';
230
    }
231
232 67
    protected function resolveEntry(RegistryEntry $entry): mixed
233
    {
234 67
        if ($entry->shouldReturnAsIs()) {
235 2
            return $entry->definition();
236
        }
237
238
        /** @var mixed - the current value, instantiated or definition */
239 66
        $value = $entry->get();
240
241 66
        if (is_string($value)) {
242 58
            if (class_exists($value)) {
243 57
                $args = $entry->getArgs();
244
245 57
                if (isset($args)) {
246
                    // Don't autowire if $args are given
247 4
                    if ($args instanceof Closure) {
248 1
                        return $this->reifyAndReturn($entry, $this->fromArgsClosure($value, $args));
249
                    }
250
251 3
                    return $this->reifyAndReturn($entry, $this->fromArgsArray($value, $args));
252
                }
253
254 53
                return $this->reifyAndReturn($entry, $this->autowire($value));
255
            }
256
257 2
            if (isset($this->entries[$value])) {
258 1
                return $this->get($value);
259
            }
260
        }
261
262 51
        if ($value instanceof Closure) {
263
            // Get the instance from the registered closure
264 9
            $rf = new ReflectionFunction($value);
265 9
            $args = $entry->getArgs();
266
267 9
            if (is_null($args)) {
268 7
                $args = $this->resolveArgs($rf);
269 2
            } elseif ($args instanceof Closure) {
270
                /** @var array<string, mixed> */
271 1
                $args = $args();
272
            }
273
274
            /** @var mixed */
275 9
            $result = $value(...$args);
276
277 9
            return $this->reifyAndReturn($entry, $result);
278
        }
279
280 48
        if (is_object($value)) {
281 47
            return $value;
282
        }
283
284 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

284
        throw new NotFoundException('Unresolvable id: ' . /** @scrutinizer ignore-type */ print_r($value, true));
Loading history...
285
    }
286
287 60
    protected function reifyAndReturn(RegistryEntry $entry, mixed $value): mixed
288
    {
289 60
        if ($entry->shouldReify()) {
290 59
            $entry->set($value);
291
        }
292
293 60
        return $value;
294
    }
295
296
    /** @param class-string $class */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
297 59
    protected function autowire(string $class): object
298
    {
299 59
        $rc = new ReflectionClass($class);
300 59
        $constructor = $rc->getConstructor();
301 59
        $args = $this->resolveArgs($constructor);
302
303
        try {
304 56
            return $rc->newInstance(...$args);
305 2
        } catch (Throwable $e) {
306 2
            throw new ContainerException(
307 2
                'Autowiring unresolvable: ' . $class . ' Details: ' . $e->getMessage()
308 2
            );
309
        }
310
    }
311
312 62
    protected function resolveArgs(?ReflectionFunctionAbstract $rf): array
313
    {
314 62
        $args = [];
315
316 62
        if ($rf) {
317 15
            foreach ($rf->getParameters() as $param) {
318
                /** @var list<mixed> */
319 9
                $args[] = $this->resolveParam($param);
320
            }
321
        }
322
323 59
        return $args;
324
    }
325
326
    /** @param class-string $class */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
327 3
    protected function fromArgsArray(string $class, array $args): object
328
    {
329
        /** @psalm-suppress MixedMethodCall */
330 3
        return new $class(...$args);
331
    }
332
333
    /** @param class-string $class */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
334 1
    protected function fromArgsClosure(string $class, Closure $callback): object
335
    {
336 1
        $rf = new ReflectionFunction($callback);
337 1
        $args = $this->resolveArgs($rf);
338
339
        /** @psalm-suppress MixedMethodCall */
340 1
        return new $class(...$callback(...$args));
341
    }
342
343 120
    protected function normalizeParameterName(string $paramName): string
344
    {
345 120
        if (empty($paramName)) {
346 120
            return $paramName;
347
        }
348
349 22
        $paramName = trim($paramName);
350
351 22
        return str_starts_with($paramName, '$') ? $paramName : '$' . $paramName;
352
    }
353
}
354