Passed
Pull Request — master (#1115)
by Maxim
22:29
created

Resolver::processArgument()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 20
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 20
ccs 9
cts 9
cp 1
rs 9.6111
c 0
b 0
f 0
cc 5
nc 4
nop 4
crap 5
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 1379
    public function __construct(Registry $constructor)
42
    {
43 1379
        $constructor->set('resolver', $this);
44
45 1379
        $this->factory = $constructor->get('factory', FactoryInterface::class);
46 1379
        $this->container = $constructor->get('container', ContainerInterface::class);
47
    }
48
49 1102
    public function resolveArguments(
50
        ContextFunction $reflection,
51
        array $parameters = [],
52
        bool $validate = true,
53
    ): array {
54 1102
        $state = new ResolvingState($reflection, $parameters);
55
56 1102
        foreach ($reflection->getParameters() as $parameter) {
57 1071
            $this->resolveParameter($parameter, $state, $validate)
58 1071
            or
59 1071
            throw new ArgumentResolvingException($reflection, $parameter->getName());
60
        }
61
62 1067
        return $state->getResolvedValues();
63
    }
64
65 44
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
66
    {
67 44
        $positional = true;
68 44
        $variadic = false;
69 44
        $parameters = $reflection->getParameters();
70 44
        if (\count($parameters) === 0) {
71 2
            return;
72
        }
73
74 42
        $parameter = null;
75 42
        while (\count($parameters) > 0 || \count($arguments) > 0) {
76
            // get related argument value
77 42
            $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 42
            if (\is_int($key) && !$positional) {
82 2
                throw new PositionalArgumentException($reflection, $key);
83
            }
84
85 42
            $positional = $positional && \is_int($key);
86
87 42
            if (!$variadic) {
88 42
                $parameter = \array_shift($parameters);
89 42
                $variadic = $parameter?->isVariadic() ?? false;
90
            }
91
92 42
            if ($parameter === null) {
93 1
                throw new UnknownParameterException($reflection, $key);
94
            }
95 42
            $name = $parameter->getName();
96
97 42
            if (($positional || $variadic) && $key !== null) {
98
                /** @psalm-suppress ReferenceReusedFromConfusingScope */
99 39
                $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 41
            if (!$this->validateValueToParameter($parameter, $value)) {
111 6
                throw new InvalidArgumentException($reflection, $name);
112
            }
113
        }
114
    }
115
116 1040
    private function validateValueToParameter(ReflectionParameter $parameter, mixed $value): bool
117
    {
118 1040
        if (!$parameter->hasType() || ($parameter->allowsNull() && $value === null)) {
119 164
            return true;
120
        }
121 1036
        $type = $parameter->getType();
122
123 1036
        [$or, $types] = match (true) {
124 1036
            $type instanceof ReflectionNamedType => [true, [$type]],
125 19
            $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 4
            $type instanceof ReflectionIntersectionType => [false, $type->getTypes()],
127 1036
        };
128
129 1036
        foreach ($types as $t) {
130 1036
            \assert($t instanceof ReflectionNamedType);
131 1036
            if (!$this->validateValueNamedType($t, $value)) {
132
                // If it is TypeIntersection
133 23
                if ($or) {
134 22
                    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 1024
            if ($or) {
140 1020
                return true;
141
            }
142
        }
143 22
        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 1036
    private function validateValueNamedType(ReflectionNamedType $type, mixed $value): bool
151
    {
152 1036
        $name = $type->getName();
153
154 1036
        if ($type->isBuiltin()) {
155 581
            return match ($name) {
156 6
                'mixed' => true,
157 516
                'string' => \is_string($value),
158 73
                'int' => \is_int($value),
159 442
                'bool' => \is_bool($value),
160 111
                'array' => \is_array($value),
161 2
                'callable' => \is_callable($value),
162 2
                'iterable' => \is_iterable($value),
163 8
                'float' => \is_float($value),
164 5
                'object' => \is_object($value),
165
                'true' => $value === true,
166
                'false' => $value === false,
167 581
                default => false,
168 581
            };
169
        }
170
171 981
        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 1071
    private function resolveParameter(ReflectionParameter $param, ResolvingState $state, bool $validate): bool
181
    {
182 1071
        $isVariadic = $param->isVariadic();
183 1071
        $hasType = $param->hasType();
184
185
        // Try to resolve parameter by name
186 1071
        $res = $state->resolveParameterByNameOrPosition($param, $isVariadic);
187 1070
        if ($res !== [] || $isVariadic) {
188
            // validate
189 586
            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 573
                $this->processArgument($state, $res[0], validateWith: $validate ? $param : null);
195
            }
196
197 578
            return true;
198
        }
199
200 1024
        $error = null;
201 1024
        while ($hasType) {
202
            /** @var ReflectionIntersectionType|ReflectionUnionType|ReflectionNamedType $refType */
203 1020
            $refType = $param->getType();
204
205 1020
            if ($refType::class === ReflectionNamedType::class) {
206 1016
                if ($refType->isBuiltin()) {
207 519
                    break;
208
                }
209
210 1001
                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 1001
                    $attrs = $param->getAttributes(ProxyAttribute::class)
212
                )) {
213 519
                    $proxy = Proxy::create(
214 519
                        new \ReflectionClass($refType->getName()),
215 519
                        $param,
216 519
                        $attrs[0]->newInstance()
217 519
                    );
218 519
                    $this->processArgument($state, $proxy);
219 519
                    return true;
220
                }
221
222
                try {
223 979
                    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 959
                        return true;
225
                    }
226 723
                } catch (Throwable $e) {
227 723
                    $error = $e;
228
                }
229 723
                break;
230
            }
231
232 9
            if ($refType::class === ReflectionUnionType::class) {
233 8
                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 8
                        if (!$namedType->isBuiltin() && $this->resolveObject($state, $namedType, $param, $validate)) {
236 8
                            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 794
        if ($param->isDefaultValueAvailable()) {
249 764
            $argument = $param->getDefaultValue();
250 764
            $this->processArgument($state, $argument);
251 764
            return true;
252
        }
253
254 38
        if ($hasType && $param->allowsNull()) {
255 9
            $argument = null;
256 9
            $this->processArgument($state, $argument);
257 9
            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 981
    private function resolveObject(
275
        ResolvingState $state,
276
        ReflectionNamedType $type,
277
        ReflectionParameter $parameter,
278
        bool $validateWith = false,
279
    ): bool {
280
        /** @psalm-suppress TooManyArguments */
281 981
        $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 963
        $this->processArgument($state, $argument, $validateWith ? $parameter : null);
283 961
        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 1051
    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 1051
        if ($value instanceof Autowire) {
302 2
            $value = $value->resolve($this->factory);
303
        }
304
305
        // Validation
306 1051
        if ($validateWith !== null && !$this->validateValueToParameter($validateWith, $value)) {
307 14
            throw new InvalidArgumentException(
308 14
                $validateWith->getDeclaringFunction(),
309 14
                $validateWith->getName()
310 14
            );
311
        }
312
313 1043
        $state->addResolvedValue($value, \is_string($key) ? $key : null);
314
    }
315
}
316