Test Failed
Pull Request — master (#1045)
by
unknown
12:32 queued 06:03
created

Resolver::resolveObject()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 10
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 1
nop 4
crap 2
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 1288
41
    public function __construct(Registry $constructor)
42 1288
    {
43
        $constructor->set('resolver', $this);
44 1288
45 1288
        $this->factory = $constructor->get('factory', FactoryInterface::class);
46
        $this->container = $constructor->get('container', ContainerInterface::class);
47
    }
48 994
49
    public function resolveArguments(
50
        ContextFunction $reflection,
51
        array $parameters = [],
52
        bool $validate = true,
53 994
    ): array {
54
        $state = new ResolvingState($reflection, $parameters);
55 994
56 971
        foreach ($reflection->getParameters() as $parameter) {
57 971
            $this->resolveParameter($parameter, $state, $validate)
58 971
            or
59
            throw new ArgumentResolvingException($reflection, $parameter->getName());
60
        }
61 956
62
        return $state->getResolvedValues();
63
    }
64 32
65
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
66 32
    {
67 32
        $positional = true;
68 32
        $variadic = false;
69 32
        $parameters = $reflection->getParameters();
70 2
        if (\count($parameters) === 0) {
71
            return;
72
        }
73 30
74 30
        $parameter = null;
75
        while (\count($parameters) > 0 || \count($arguments) > 0) {
76 30
            // get related argument value
77
            $key = \key($arguments);
78
79
            // For a variadic parameter it's no sense - named or positional argument will be sent
80 30
            // But you can't send positional argument after named in any case
81 2
            if (\is_int($key) && !$positional) {
82
                throw new PositionalArgumentException($reflection, $key);
83
            }
84 30
85
            $positional = $positional && \is_int($key);
86 30
87 30
            if (!$variadic) {
88 30
                $parameter = \array_shift($parameters);
89
                $variadic = $parameter?->isVariadic() ?? false;
90
            }
91 30
92 1
            if ($parameter === null) {
93
                throw new UnknownParameterException($reflection, $key);
94 30
            }
95
            $name = $parameter->getName();
96 30
97
            if (($positional || $variadic) && $key !== null) {
98 27
                /** @psalm-suppress ReferenceReusedFromConfusingScope */
99 9
                $value = \array_shift($arguments);
100 4
            } elseif ($key === null || !\array_key_exists($name, $arguments)) {
101 3
                if ($parameter->isOptional()) {
102
                    continue;
103 1
                }
104
                throw new MissingRequiredArgumentException($reflection, $name);
105 6
            } else {
106 6
                $value = &$arguments[$name];
107
                unset($arguments[$name]);
108
            }
109 29
110 6
            if (!$this->validateValueToParameter($parameter, $value)) {
111
                throw new InvalidArgumentException($reflection, $name);
112
            }
113
        }
114
    }
115 959
116
    private function validateValueToParameter(ReflectionParameter $parameter, mixed $value): bool
117 959
    {
118 99
        if (!$parameter->hasType() || ($parameter->allowsNull() && $value === null)) {
119
            return true;
120 955
        }
121
        $type = $parameter->getType();
122 955
123 955
        [$or, $types] = match (true) {
124 955
            $type instanceof ReflectionNamedType => [true, [$type]],
125 955
            $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 955
            $type instanceof ReflectionIntersectionType => [false, $type->getTypes()],
127
        };
128 955
129 955
        foreach ($types as $t) {
130 955
            \assert($t instanceof ReflectionNamedType);
131
            if (!$this->validateValueNamedType($t, $value)) {
132 20
                // If it is TypeIntersection
133 19
                if ($or) {
134
                    continue;
135 1
                }
136
                return false;
137
            }
138 943
            // If it is not type intersection then we can skip that value after first successful check
139 939
            if ($or) {
140
                return true;
141
            }
142 20
        }
143
        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 955
     */
150
    private function validateValueNamedType(ReflectionNamedType $type, mixed $value): bool
151 955
    {
152
        $name = $type->getName();
153 955
154 508
        if ($type->isBuiltin()) {
155 508
            return match ($name) {
156 508
                'mixed' => true,
157 508
                'string' => \is_string($value),
158 508
                'int' => \is_int($value),
159 508
                'bool' => \is_bool($value),
160 508
                'array' => \is_array($value),
161 508
                'callable' => \is_callable($value),
162 508
                'iterable' => \is_iterable($value),
163 508
                'float' => \is_float($value),
164 508
                'object' => \is_object($value),
165 508
                'true' => $value === true,
166 508
                'false' => $value === false,
167 508
                default => false,
168
            };
169
        }
170 898
171
        return $value instanceof $name;
172
    }
173
174
    /**
175
     * Returns {@see true} if argument was resolved.
176
     *
177
     * @throws ResolvingException
178
     * @throws NotFoundExceptionInterface|ContainerExceptionInterface
179 971
     */
180
    private function resolveParameter(ReflectionParameter $param, ResolvingState $state, bool $validate): bool
181 971
    {
182 971
        $isVariadic = $param->isVariadic();
183
        $hasType = $param->hasType();
184
185 971
        // Try to resolve parameter by name
186 970
        $res = $state->resolveParameterByNameOrPosition($param, $isVariadic);
187
        if ($res !== [] || $isVariadic) {
188 518
            // validate
189 15
            if ($isVariadic) {
190 13
                foreach ($res as $k => &$v) {
191
                    $this->processArgument($state, $v, validateWith: $validate ? $param : null, key: $k);
192
                }
193 505
            } else {
194
                $this->processArgument($state, $res[0], validateWith: $validate ? $param : null);
195
            }
196 512
197
            return true;
198
        }
199 923
200 923
        $error = null;
201
        while ($hasType) {
202 920
            /** @var ReflectionIntersectionType|ReflectionUnionType|ReflectionNamedType $refType */
203
            $refType = $param->getType();
204 920
205 1
            if ($refType::class === ReflectionNamedType::class) {
206
                if ($refType->isBuiltin()) {
207
                    break;
208 919
                }
209 919
210
                if (\interface_exists($refType->getName()) && !empty($param->getAttributes(ProxyAttribute::class))) {
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($param->getAttributes(ProxyAttribute::class))) {

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 919
                    $proxy = Proxy::create(new \ReflectionClass($refType->getName()), $param);
212 901
                    $this->processArgument($state, $proxy);
213
                    return true;
214 658
                }
215 658
216
                try {
217
                    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

217
                    if ($this->resolveObject($state, /** @scrutinizer ignore-type */ $refType, $param, $validate)) {
Loading history...
218
                        return true;
219
                    }
220 732
                } catch (Throwable $e) {
221 699
                    $error = $e;
222 699
                }
223 699
                break;
224
            }
225
226 35
            if ($refType::class === ReflectionUnionType::class) {
227 6
                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

227
                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...
228 6
                    try {
229 6
                        if (!$namedType->isBuiltin() && $this->resolveObject($state, $namedType, $param, $validate)) {
230
                            return true;
231
                        }
232 29
                    } catch (Throwable $e) {
233 10
                        $error = $e;
234
                    }
235
                }
236
                break;
237 22
            }
238
239
            throw new UnsupportedTypeException($param->getDeclaringFunction(), $param->getName());
240
        }
241
242
        if ($param->isDefaultValueAvailable()) {
243
            $argument = $param->getDefaultValue();
244
            $this->processArgument($state, $argument);
245
            return true;
246
        }
247
248 919
        if ($hasType && $param->allowsNull()) {
249
            $argument = null;
250
            $this->processArgument($state, $argument);
251
            return true;
252
        }
253
254 919
        if ($error === null) {
255 919
            return false;
256 919
        }
257 919
258 919
        // Throw NotFoundExceptionInterface
259 919
        throw $error;
260
    }
261
262
    /**
263
     * Resolve argument by class name and context. Returns {@see true} if argument resolved.
264
     *
265
     * @throws ContainerExceptionInterface
266
     * @throws NotFoundExceptionInterface
267
     */
268
    private function resolveObject(
269
        ResolvingState $state,
270 899
        ReflectionNamedType $type,
271
        ReflectionParameter $parameter,
272
        bool $validateWith = false,
273
    ): bool {
274
        /** @psalm-suppress TooManyArguments */
275
        $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

275
        /** @scrutinizer ignore-call */ 
276
        $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...
276
        $this->processArgument($state, $argument, $validateWith ? $parameter : null);
277 899
        return true;
278 880
    }
279 878
280
    /**
281
     * Arguments processing. {@see Autowire} object will be resolved.
282
     *
283
     * @param mixed $value Resolved value.
284
     * @param ReflectionParameter|null $validateWith Should be passed when the value should be validated.
285
     *        Must be set for when value is user's argument.
286
     * @param int|string|null $key Only {@see string} values will be preserved.
287
     */
288
    private function processArgument(
289
        ResolvingState $state,
290 948
        mixed &$value,
291
        ReflectionParameter $validateWith = null,
292
        int|string $key = null
293
    ): void {
294
        // Resolve Autowire objects
295
        if ($value instanceof Autowire) {
296
            $value = $value->resolve($this->factory);
297 948
        }
298 2
299
        // Validation
300
        if ($validateWith !== null && !$this->validateValueToParameter($validateWith, $value)) {
301
            throw new InvalidArgumentException(
302 948
                $validateWith->getDeclaringFunction(),
303 12
                $validateWith->getName()
304 12
            );
305 12
        }
306 12
307
        $state->addResolvedValue($value, \is_string($key) ? $key : null);
308
    }
309
}
310