Passed
Push — master ( 7bd689...ed0e19 )
by butschster
16:50 queued 18s
created

Resolver   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 60
eloc 133
c 1
b 0
f 0
dl 0
loc 280
ccs 137
cts 137
cp 1
rs 3.6

8 Methods

Rating   Name   Duplication   Size   Complexity  
A processArgument() 0 20 5
A validateValueNamedType() 0 22 2
C validateArguments() 0 47 16
A __construct() 0 6 1
B validateValueToParameter() 0 28 8
A resolveArguments() 0 14 3
A resolveObject() 0 10 2
D resolveParameter() 0 86 23

How to fix   Complexity   

Complex Class

Complex classes like Resolver 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 Resolver, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Core\Internal;
6
7
use Psr\Container\ContainerExceptionInterface;
8
use Psr\Container\ContainerInterface;
9
use Psr\Container\NotFoundExceptionInterface;
10
use ReflectionFunctionAbstract as ContextFunction;
11
use ReflectionIntersectionType;
0 ignored issues
show
Bug introduced by
The type ReflectionIntersectionType 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...
12
use ReflectionNamedType;
13
use ReflectionParameter;
14
use ReflectionUnionType;
15
use Spiral\Core\Attribute\Proxy as ProxyAttribute;
16
use Spiral\Core\Container\Autowire;
17
use Spiral\Core\Exception\Resolver\ArgumentResolvingException;
18
use Spiral\Core\Exception\Resolver\InvalidArgumentException;
19
use Spiral\Core\Exception\Resolver\MissingRequiredArgumentException;
20
use Spiral\Core\Exception\Resolver\PositionalArgumentException;
21
use Spiral\Core\Exception\Resolver\ResolvingException;
22
use Spiral\Core\Exception\Resolver\UnknownParameterException;
23
use Spiral\Core\Exception\Resolver\UnsupportedTypeException;
24
use Spiral\Core\FactoryInterface;
25
use Spiral\Core\Internal\Common\DestructorTrait;
26
use Spiral\Core\Internal\Common\Registry;
27
use Spiral\Core\Internal\Resolver\ResolvingState;
28
use Spiral\Core\ResolverInterface;
29
use Throwable;
30
31
/**
32
 * @internal
33
 */
34
final class Resolver implements ResolverInterface
35
{
36
    use DestructorTrait;
37
38
    private FactoryInterface $factory;
39
    private ContainerInterface $container;
40
41 1311
    public function __construct(Registry $constructor)
42
    {
43 1311
        $constructor->set('resolver', $this);
44
45 1311
        $this->factory = $constructor->get('factory', FactoryInterface::class);
46 1311
        $this->container = $constructor->get('container', ContainerInterface::class);
47
    }
48
49 1015
    public function resolveArguments(
50
        ContextFunction $reflection,
51
        array $parameters = [],
52
        bool $validate = true,
53
    ): array {
54 1015
        $state = new ResolvingState($reflection, $parameters);
55
56 1015
        foreach ($reflection->getParameters() as $parameter) {
57 991
            $this->resolveParameter($parameter, $state, $validate)
58 991
            or
59 991
            throw new ArgumentResolvingException($reflection, $parameter->getName());
60
        }
61
62 977
        return $state->getResolvedValues();
63
    }
64
65 37
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
66
    {
67 37
        $positional = true;
68 37
        $variadic = false;
69 37
        $parameters = $reflection->getParameters();
70 37
        if (\count($parameters) === 0) {
71 2
            return;
72
        }
73
74 35
        $parameter = null;
75 35
        while (\count($parameters) > 0 || \count($arguments) > 0) {
76
            // get related argument value
77 35
            $key = \key($arguments);
78
79
            // For a variadic parameter it's no sense - named or positional argument will be sent
80
            // But you can't send positional argument after named in any case
81 35
            if (\is_int($key) && !$positional) {
82 2
                throw new PositionalArgumentException($reflection, $key);
83
            }
84
85 35
            $positional = $positional && \is_int($key);
86
87 35
            if (!$variadic) {
88 35
                $parameter = \array_shift($parameters);
89 35
                $variadic = $parameter?->isVariadic() ?? false;
90
            }
91
92 35
            if ($parameter === null) {
93 1
                throw new UnknownParameterException($reflection, $key);
94
            }
95 35
            $name = $parameter->getName();
96
97 35
            if (($positional || $variadic) && $key !== null) {
98
                /** @psalm-suppress ReferenceReusedFromConfusingScope */
99 32
                $value = \array_shift($arguments);
100 9
            } elseif ($key === null || !\array_key_exists($name, $arguments)) {
101 4
                if ($parameter->isOptional()) {
102 3
                    continue;
103
                }
104 1
                throw new MissingRequiredArgumentException($reflection, $name);
105
            } else {
106 6
                $value = &$arguments[$name];
107 6
                unset($arguments[$name]);
108
            }
109
110 34
            if (!$this->validateValueToParameter($parameter, $value)) {
111 6
                throw new InvalidArgumentException($reflection, $name);
112
            }
113
        }
114
    }
115
116 969
    private function validateValueToParameter(ReflectionParameter $parameter, mixed $value): bool
117
    {
118 969
        if (!$parameter->hasType() || ($parameter->allowsNull() && $value === null)) {
119 99
            return true;
120
        }
121 965
        $type = $parameter->getType();
122
123 965
        [$or, $types] = match (true) {
124 965
            $type instanceof ReflectionNamedType => [true, [$type]],
125 965
            $type instanceof ReflectionUnionType => [true, $type->getTypes()],
0 ignored issues
show
Bug introduced by
The method getTypes() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionUnionType. ( Ignorable by Annotation )

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

125
            $type instanceof ReflectionUnionType => [true, $type->/** @scrutinizer ignore-call */ getTypes()],
Loading history...
126 965
            $type instanceof ReflectionIntersectionType => [false, $type->getTypes()],
127 965
        };
128
129 965
        foreach ($types as $t) {
130 965
            \assert($t instanceof ReflectionNamedType);
131 965
            if (!$this->validateValueNamedType($t, $value)) {
132
                // If it is TypeIntersection
133 21
                if ($or) {
134 20
                    continue;
135
                }
136 1
                return false;
137
            }
138
            // If it is not type intersection then we can skip that value after first successful check
139 953
            if ($or) {
140 949
                return true;
141
            }
142
        }
143 20
        return !$or;
144
    }
145
146
    /**
147
     * Validate the value have the same type that in the $type.
148
     * This method doesn't resolve cases with nullable type and {@see null} value.
149
     */
150 965
    private function validateValueNamedType(ReflectionNamedType $type, mixed $value): bool
151
    {
152 965
        $name = $type->getName();
153
154 965
        if ($type->isBuiltin()) {
155 513
            return match ($name) {
156 513
                'mixed' => true,
157 513
                'string' => \is_string($value),
158 513
                'int' => \is_int($value),
159 513
                'bool' => \is_bool($value),
160 513
                'array' => \is_array($value),
161 513
                'callable' => \is_callable($value),
162 513
                'iterable' => \is_iterable($value),
163 513
                'float' => \is_float($value),
164 513
                'object' => \is_object($value),
165 513
                'true' => $value === true,
166 513
                'false' => $value === false,
167 513
                default => false,
168 513
            };
169
        }
170
171 908
        return $value instanceof $name;
172
    }
173
174
    /**
175
     * Returns {@see true} if argument was resolved.
176
     *
177
     * @throws ResolvingException
178
     * @throws NotFoundExceptionInterface|ContainerExceptionInterface
179
     */
180 991
    private function resolveParameter(ReflectionParameter $param, ResolvingState $state, bool $validate): bool
181
    {
182 991
        $isVariadic = $param->isVariadic();
183 991
        $hasType = $param->hasType();
184
185
        // Try to resolve parameter by name
186 991
        $res = $state->resolveParameterByNameOrPosition($param, $isVariadic);
187 990
        if ($res !== [] || $isVariadic) {
188
            // validate
189 518
            if ($isVariadic) {
190 15
                foreach ($res as $k => &$v) {
191 13
                    $this->processArgument($state, $v, validateWith: $validate ? $param : null, key: $k);
192
                }
193
            } else {
194 505
                $this->processArgument($state, $res[0], validateWith: $validate ? $param : null);
195
            }
196
197 512
            return true;
198
        }
199
200 943
        $error = null;
201 943
        while ($hasType) {
202
            /** @var ReflectionIntersectionType|ReflectionUnionType|ReflectionNamedType $refType */
203 940
            $refType = $param->getType();
204
205 940
            if ($refType::class === ReflectionNamedType::class) {
206 936
                if ($refType->isBuiltin()) {
207 454
                    break;
208
                }
209
210 917
                if (\interface_exists($refType->getName()) && !empty(
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionUnionType. ( Ignorable by Annotation )

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

210
                if (\interface_exists($refType->/** @scrutinizer ignore-call */ getName()) && !empty(

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...
211 917
                    $attrs = $param->getAttributes(ProxyAttribute::class)
212
                )) {
213 15
                    $proxy = Proxy::create(
214 15
                        new \ReflectionClass($refType->getName()),
215 15
                        $param,
216 15
                        $attrs[0]->newInstance()
217 15
                    );
218 15
                    $this->processArgument($state, $proxy);
219 15
                    return true;
220
                }
221
222
                try {
223 907
                    if ($this->resolveObject($state, $refType, $param, $validate)) {
0 ignored issues
show
Bug introduced by
It seems like $refType can also be of type ReflectionUnionType; however, parameter $type of Spiral\Core\Internal\Resolver::resolveObject() does only seem to accept ReflectionNamedType, maybe add an additional type check? ( Ignorable by Annotation )

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

223
                    if ($this->resolveObject($state, /** @scrutinizer ignore-type */ $refType, $param, $validate)) {
Loading history...
224 886
                        return true;
225
                    }
226 658
                } catch (Throwable $e) {
227 658
                    $error = $e;
228
                }
229 658
                break;
230
            }
231
232 8
            if ($refType::class === ReflectionUnionType::class) {
233 7
                foreach ($refType->getTypes() as $namedType) {
0 ignored issues
show
Bug introduced by
The method getTypes() does not exist on ReflectionNamedType. ( Ignorable by Annotation )

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

233
                foreach ($refType->/** @scrutinizer ignore-call */ getTypes() as $namedType) {

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...
234
                    try {
235 7
                        if (!$namedType->isBuiltin() && $this->resolveObject($state, $namedType, $param, $validate)) {
236 7
                            return true;
237
                        }
238 4
                    } catch (Throwable $e) {
239 4
                        $error = $e;
240
                    }
241
                }
242 5
                break;
243
            }
244
245 1
            throw new UnsupportedTypeException($param->getDeclaringFunction(), $param->getName());
246
        }
247
248 732
        if ($param->isDefaultValueAvailable()) {
249 699
            $argument = $param->getDefaultValue();
250 699
            $this->processArgument($state, $argument);
251 699
            return true;
252
        }
253
254 35
        if ($hasType && $param->allowsNull()) {
255 6
            $argument = null;
256 6
            $this->processArgument($state, $argument);
257 6
            return true;
258
        }
259
260 29
        if ($error === null) {
261 10
            return false;
262
        }
263
264
        // Throw NotFoundExceptionInterface
265 22
        throw $error;
266
    }
267
268
    /**
269
     * Resolve argument by class name and context. Returns {@see true} if argument resolved.
270
     *
271
     * @throws ContainerExceptionInterface
272
     * @throws NotFoundExceptionInterface
273
     */
274 909
    private function resolveObject(
275
        ResolvingState $state,
276
        ReflectionNamedType $type,
277
        ReflectionParameter $parameter,
278
        bool $validateWith = false,
279
    ): bool {
280
        /** @psalm-suppress TooManyArguments */
281 909
        $argument = $this->container->get($type->getName(), $parameter);
0 ignored issues
show
Unused Code introduced by
The call to Psr\Container\ContainerInterface::get() has too many arguments starting with $parameter. ( Ignorable by Annotation )

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

281
        /** @scrutinizer ignore-call */ 
282
        $argument = $this->container->get($type->getName(), $parameter);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
282 890
        $this->processArgument($state, $argument, $validateWith ? $parameter : null);
283 888
        return true;
284
    }
285
286
    /**
287
     * Arguments processing. {@see Autowire} object will be resolved.
288
     *
289
     * @param mixed $value Resolved value.
290
     * @param ReflectionParameter|null $validateWith Should be passed when the value should be validated.
291
     *        Must be set for when value is user's argument.
292
     * @param int|string|null $key Only {@see string} values will be preserved.
293
     */
294 968
    private function processArgument(
295
        ResolvingState $state,
296
        mixed &$value,
297
        ReflectionParameter $validateWith = null,
298
        int|string $key = null
299
    ): void {
300
        // Resolve Autowire objects
301 968
        if ($value instanceof Autowire) {
302 2
            $value = $value->resolve($this->factory);
303
        }
304
305
        // Validation
306 968
        if ($validateWith !== null && !$this->validateValueToParameter($validateWith, $value)) {
307 12
            throw new InvalidArgumentException(
308 12
                $validateWith->getDeclaringFunction(),
309 12
                $validateWith->getName()
310 12
            );
311
        }
312
313 960
        $state->addResolvedValue($value, \is_string($key) ? $key : null);
314
    }
315
}
316