Test Failed
Pull Request — master (#1041)
by Aleksei
06:38
created

Resolver::resolveObject()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 10
ccs 3
cts 3
cp 1
rs 10
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\Container\Autowire;
16
use Spiral\Core\Exception\Resolver\ArgumentResolvingException;
17
use Spiral\Core\Exception\Resolver\InvalidArgumentException;
18
use Spiral\Core\Exception\Resolver\MissingRequiredArgumentException;
19
use Spiral\Core\Exception\Resolver\PositionalArgumentException;
20
use Spiral\Core\Exception\Resolver\ResolvingException;
21
use Spiral\Core\Exception\Resolver\UnknownParameterException;
22
use Spiral\Core\Exception\Resolver\UnsupportedTypeException;
23
use Spiral\Core\FactoryInterface;
24
use Spiral\Core\Internal\Common\DestructorTrait;
25
use Spiral\Core\Internal\Common\Registry;
26
use Spiral\Core\Internal\Resolver\ResolvingState;
27
use Spiral\Core\ResolverInterface;
28
use Throwable;
29
30
/**
31
 * @internal
32
 */
33
final class Resolver implements ResolverInterface
34
{
35
    use DestructorTrait;
36
37
    private FactoryInterface $factory;
38
    private ContainerInterface $container;
39
40 1285
    public function __construct(Registry $constructor)
41
    {
42 1285
        $constructor->set('resolver', $this);
43
44 1285
        $this->factory = $constructor->get('factory', FactoryInterface::class);
45 1285
        $this->container = $constructor->get('container', ContainerInterface::class);
46
    }
47
48 991
    public function resolveArguments(
49
        ContextFunction $reflection,
50
        array $parameters = [],
51
        bool $validate = true,
52
    ): array {
53 991
        $state = new ResolvingState($reflection, $parameters);
54
55 991
        foreach ($reflection->getParameters() as $parameter) {
56 968
            $this->resolveParameter($parameter, $state, $validate)
57 968
            or
58 968
            throw new ArgumentResolvingException($reflection, $parameter->getName());
59
        }
60
61 953
        return $state->getResolvedValues();
62
    }
63
64 32
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
65
    {
66 32
        $positional = true;
67 32
        $variadic = false;
68 32
        $parameters = $reflection->getParameters();
69 32
        if (\count($parameters) === 0) {
70 2
            return;
71
        }
72
73 30
        $parameter = null;
74 30
        while (\count($parameters) > 0 || \count($arguments) > 0) {
75
            // get related argument value
76 30
            $key = \key($arguments);
77
78
            // For a variadic parameter it's no sense - named or positional argument will be sent
79
            // But you can't send positional argument after named in any case
80 30
            if (\is_int($key) && !$positional) {
81 2
                throw new PositionalArgumentException($reflection, $key);
82
            }
83
84 30
            $positional = $positional && \is_int($key);
85
86 30
            if (!$variadic) {
87 30
                $parameter = \array_shift($parameters);
88 30
                $variadic = $parameter?->isVariadic() ?? false;
89
            }
90
91 30
            if ($parameter === null) {
92 1
                throw new UnknownParameterException($reflection, $key);
93
            }
94 30
            $name = $parameter->getName();
95
96 30
            if (($positional || $variadic) && $key !== null) {
97
                /** @psalm-suppress ReferenceReusedFromConfusingScope */
98 27
                $value = \array_shift($arguments);
99 9
            } elseif ($key === null || !\array_key_exists($name, $arguments)) {
100 4
                if ($parameter->isOptional()) {
101 3
                    continue;
102
                }
103 1
                throw new MissingRequiredArgumentException($reflection, $name);
104
            } else {
105 6
                $value = &$arguments[$name];
106 6
                unset($arguments[$name]);
107
            }
108
109 29
            if (!$this->validateValueToParameter($parameter, $value)) {
110 6
                throw new InvalidArgumentException($reflection, $name);
111
            }
112
        }
113
    }
114
115 956
    private function validateValueToParameter(ReflectionParameter $parameter, mixed $value): bool
116
    {
117 956
        if (!$parameter->hasType() || ($parameter->allowsNull() && $value === null)) {
118 99
            return true;
119
        }
120 952
        $type = $parameter->getType();
121
122 952
        [$or, $types] = match (true) {
123 952
            $type instanceof ReflectionNamedType => [true, [$type]],
124 952
            $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

124
            $type instanceof ReflectionUnionType => [true, $type->/** @scrutinizer ignore-call */ getTypes()],
Loading history...
125 952
            $type instanceof ReflectionIntersectionType => [false, $type->getTypes()],
126 952
        };
127
128 952
        foreach ($types as $t) {
129 952
            \assert($t instanceof ReflectionNamedType);
130 952
            if (!$this->validateValueNamedType($t, $value)) {
131
                // If it is TypeIntersection
132 20
                if ($or) {
133 19
                    continue;
134
                }
135 1
                return false;
136
            }
137
            // If it is not type intersection then we can skip that value after first successful check
138 940
            if ($or) {
139 936
                return true;
140
            }
141
        }
142 20
        return !$or;
143
    }
144
145
    /**
146
     * Validate the value have the same type that in the $type.
147
     * This method doesn't resolve cases with nullable type and {@see null} value.
148
     */
149 952
    private function validateValueNamedType(ReflectionNamedType $type, mixed $value): bool
150
    {
151 952
        $name = $type->getName();
152
153 952
        if ($type->isBuiltin()) {
154 505
            return match ($name) {
155 505
                'mixed' => true,
156 505
                'string' => \is_string($value),
157 505
                'int' => \is_int($value),
158 505
                'bool' => \is_bool($value),
159 505
                'array' => \is_array($value),
160 505
                'callable' => \is_callable($value),
161 505
                'iterable' => \is_iterable($value),
162 505
                'float' => \is_float($value),
163 505
                'object' => \is_object($value),
164 505
                'true' => $value === true,
165 505
                'false' => $value === false,
166 505
                default => false,
167 505
            };
168
        }
169
170 895
        return $value instanceof $name;
171
    }
172
173
    /**
174
     * Returns {@see true} if argument was resolved.
175
     *
176
     * @throws ResolvingException
177
     * @throws NotFoundExceptionInterface|ContainerExceptionInterface
178
     */
179 968
    private function resolveParameter(ReflectionParameter $parameter, ResolvingState $state, bool $validate): bool
180
    {
181 968
        $isVariadic = $parameter->isVariadic();
182 968
        $hasType = $parameter->hasType();
183
184
        // Try to resolve parameter by name
185 968
        $res = $state->resolveParameterByNameOrPosition($parameter, $isVariadic);
186 967
        if ($res !== [] || $isVariadic) {
187
            // validate
188 515
            if ($isVariadic) {
189 15
                foreach ($res as $k => &$v) {
190 13
                    $this->processArgument($state, $v, validateWith: $validate ? $parameter : null, key: $k);
191
                }
192
            } else {
193 502
                $this->processArgument($state, $res[0], validateWith: $validate ? $parameter : null);
194
            }
195
196 509
            return true;
197
        }
198
199 920
        $error = null;
200 920
        if ($hasType) {
201
            /** @var ReflectionIntersectionType|ReflectionUnionType|ReflectionNamedType $reflectionType */
202 917
            $reflectionType = $parameter->getType();
203
204 917
            if ($reflectionType instanceof ReflectionIntersectionType) {
205 1
                throw new UnsupportedTypeException($parameter->getDeclaringFunction(), $parameter->getName());
206
            }
207
208 916
            $types = $reflectionType instanceof ReflectionNamedType ? [$reflectionType] : $reflectionType->getTypes();
209 916
            foreach ($types as $namedType) {
210
                try {
211 916
                    if (!$namedType->isBuiltin() && $this->resolveObject($state, $namedType, $parameter, $validate)) {
212 898
                        return true;
213
                    }
214 655
                } catch (Throwable $e) {
215 655
                    $error = $e;
216
                }
217
            }
218
        }
219
220 729
        if ($parameter->isDefaultValueAvailable()) {
221 696
            $argument = $parameter->getDefaultValue();
222 696
            $this->processArgument($state, $argument);
223 696
            return true;
224
        }
225
226 35
        if ($hasType && $parameter->allowsNull()) {
227 6
            $argument = null;
228 6
            $this->processArgument($state, $argument);
229 6
            return true;
230
        }
231
232 29
        if ($error === null) {
233 10
            return false;
234
        }
235
236
        // Throw NotFoundExceptionInterface
237 22
        throw $error;
238
    }
239
240
    /**
241
     * Resolve argument by class name and context. Returns {@see true} if argument resolved.
242
     *
243
     * @throws ContainerExceptionInterface
244
     * @throws NotFoundExceptionInterface
245
     */
246
    private function resolveObject(
247
        ResolvingState $state,
248 916
        ReflectionNamedType $type,
249
        ReflectionParameter $parameter,
250
        bool $validateWith = false,
251
    ): bool {
252
        /** @psalm-suppress TooManyArguments */
253
        $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

253
        /** @scrutinizer ignore-call */ 
254
        $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...
254 916
        $this->processArgument($state, $argument, $validateWith ? $parameter : null);
255 916
        return true;
256 916
    }
257 916
258 916
    /**
259 916
     * Arguments processing. {@see Autowire} object will be resolved.
260
     *
261
     * @param mixed $value Resolved value.
262
     * @param ReflectionParameter|null $validateWith Should be passed when the value should be validated.
263
     *        Must be set for when value is user's argument.
264
     * @param int|string|null $key Only {@see string} values will be preserved.
265
     */
266
    private function processArgument(
267
        ResolvingState $state,
268
        mixed &$value,
269
        ReflectionParameter $validateWith = null,
270 896
        int|string $key = null
271
    ): void {
272
        // Resolve Autowire objects
273
        if ($value instanceof Autowire) {
274
            $value = $value->resolve($this->factory);
275
        }
276
277 896
        // Validation
278 877
        if ($validateWith !== null && !$this->validateValueToParameter($validateWith, $value)) {
279 875
            throw new InvalidArgumentException(
280
                $validateWith->getDeclaringFunction(),
281
                $validateWith->getName()
282
            );
283
        }
284
285
        $state->addResolvedValue($value, \is_string($key) ? $key : null);
286
    }
287
}
288