Resolver::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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

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

120
            $type instanceof \ReflectionUnionType => [true, $type->/** @scrutinizer ignore-call */ getTypes()],
Loading history...
121 4
            $type instanceof \ReflectionIntersectionType => [false, $type->getTypes()],
122 1045
        };
123
124 1045
        foreach ($types as $t) {
125 1045
            \assert($t instanceof \ReflectionNamedType);
126 1045
            if (!$this->validateValueNamedType($t, $value)) {
127
                // If it is TypeIntersection
128 23
                if ($or) {
129 22
                    continue;
130
                }
131 1
                return false;
132
            }
133
            // If it is not type intersection then we can skip that value after first successful check
134 1033
            if ($or) {
135 1029
                return true;
136
            }
137
        }
138 22
        return !$or;
139
    }
140
141
    /**
142
     * Validate the value have the same type that in the $type.
143
     * This method doesn't resolve cases with nullable type and {@see null} value.
144
     */
145 1045
    private function validateValueNamedType(\ReflectionNamedType $type, mixed $value): bool
146
    {
147 1045
        $name = $type->getName();
148
149 1045
        if ($type->isBuiltin()) {
150 586
            return match ($name) {
151 6
                'mixed' => true,
152 517
                'string' => \is_string($value),
153 78
                'int' => \is_int($value),
154 443
                'bool' => \is_bool($value),
155 111
                'array' => \is_array($value),
156 2
                'callable' => \is_callable($value),
157 2
                'iterable' => \is_iterable($value),
158 8
                'float' => \is_float($value),
159 5
                'object' => \is_object($value),
160
                'true' => $value === true,
161
                'false' => $value === false,
162 586
                default => false,
163 586
            };
164
        }
165
166 987
        return $value instanceof $name;
167
    }
168
169
    /**
170
     * Returns {@see true} if argument was resolved.
171
     *
172
     * @throws ResolvingException
173
     * @throws NotFoundExceptionInterface|ContainerExceptionInterface
174
     */
175 1127
    private function resolveParameter(\ReflectionParameter $param, ResolvingState $state, bool $validate): bool
176
    {
177 1127
        $isVariadic = $param->isVariadic();
178 1127
        $hasType = $param->hasType();
179
180
        // Try to resolve parameter by name
181 1127
        $res = $state->resolveParameterByNameOrPosition($param, $isVariadic);
182 1126
        if ($res !== [] || $isVariadic) {
183
            // validate
184 593
            if ($isVariadic) {
185 15
                foreach ($res as $k => &$v) {
186 13
                    $this->processArgument($state, $v, validateWith: $validate ? $param : null, key: $k);
187
                }
188
            } else {
189 580
                $this->processArgument($state, $res[0], validateWith: $validate ? $param : null);
190
            }
191
192 585
            return true;
193
        }
194
195 1077
        $error = null;
196 1077
        while ($hasType) {
197
            /** @var \ReflectionIntersectionType|\ReflectionUnionType|\ReflectionNamedType $refType */
198 1073
            $refType = $param->getType();
199
200 1073
            if ($refType::class === \ReflectionNamedType::class) {
201 1069
                if ($refType->isBuiltin()) {
202 520
                    break;
203
                }
204
205 1054
                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

205
                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...
Bug introduced by
The method getName() does not exist on ReflectionIntersectionType. ( Ignorable by Annotation )

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

205
                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...
206 1054
                    $attrs = $param->getAttributes(ProxyAttribute::class)
207
                )) {
208 587
                    $proxy = Proxy::create(
209 587
                        new \ReflectionClass($refType->getName()),
210 587
                        $param,
211 587
                        $attrs[0]->newInstance(),
212 587
                    );
213 587
                    $this->processArgument($state, $proxy);
214 587
                    return true;
215
                }
216
217
                try {
218 1038
                    if ($this->resolveObject($state, $refType, $param, $validate)) {
0 ignored issues
show
Bug introduced by
It seems like $refType can also be of type ReflectionIntersectionType and 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

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

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

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