Test Failed
Pull Request — master (#37)
by Divine Niiquaye
03:31
created

Resolver::resolve()   D

Complexity

Conditions 24
Paths 34

Size

Total Lines 50
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 24
eloc 33
c 2
b 0
f 0
nc 34
nop 2
dl 0
loc 50
rs 4.1666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI;
19
20
use Nette\Utils\{Callback, Reflection};
21
use PhpParser\BuilderFactory;
22
use PhpParser\Node\{Expr, Stmt, Scalar};
23
use Rade\DI\Exceptions\{ContainerResolutionException, NotFoundServiceException};
24
use Symfony\Contracts\Service\{ServiceProviderInterface, ServiceSubscriberInterface};
25
26
/**
27
 * Class Resolver.
28
 *
29
 * @author Divine Niiquaye Ibok <[email protected]>
30
 */
31
class Resolver
32
{
33
    private AbstractContainer $container;
34
35
    private ?BuilderFactory $builder;
36
37
    private bool $strict = true;
38
39
    /** @var array<string,\PhpParser\Node> */
40
    private array $literalCache = [];
41
42
    public function __construct(AbstractContainer $container, BuilderFactory $builder = null)
43
    {
44
        $this->builder = $builder;
45
        $this->container = $container;
46
    }
47
48
    /**
49
     * If true, exception will be thrown on resolvable services with are not typed.
50
     */
51
    public function setStrictAutowiring(bool $boolean = true): void
52
    {
53
        $this->strict = $boolean;
54
    }
55
56
    /**
57
     * The method name generated for a service definition.
58
     */
59
    public function createMethod(string $id): string
60
    {
61
        return 'get' . \str_replace(['.', '_', '\\'], '', \ucwords($id, '._'));
62
    }
63
64
    /**
65
     * @param mixed $definition
66
     */
67
    public static function autowireService($definition, bool $allTypes = false, AbstractContainer $container = null): array
68
    {
69
        $types = $autowired = [];
70
71
        if (\is_callable($definition)) {
72
            $definition = \Closure::fromCallable($definition);
73
        }
74
75
        if ($definition instanceof \Closure) {
76
            $definition = Callback::unwrap($definition);
77
            $types = self::getTypes(\is_array($definition) ? new \ReflectionMethod($definition[0], $definition[1]) : new \ReflectionFunction($definition));
0 ignored issues
show
introduced by
The condition is_array($definition) is always true.
Loading history...
78
        } elseif (\is_string($definition)) {
79
            if (!(\class_exists($definition) || \interface_exists($definition))) {
80
                return $allTypes ? ['string'] : [];
81
            }
82
83
            $types[] = $definition;
84
        } elseif (\is_array($definition)) {
85
            if (null !== $container && 2 === \count($definition, \COUNT_RECURSIVE)) {
86
                if ($definition[0] instanceof Definitions\Reference) {
87
                    $def = $container->definition((string) $definition[0]);
88
                } elseif ($definition[0] instanceof Expr\BinaryOp\Coalesce) {
89
                    $def = $container->definition($definition[0]->left->dim->value);
90
                }
91
92
                if (isset($def)) {
93
                    if ($def instanceof Definitions\DefinitionInterface) {
94
                        $class = self::getDefinitionClass($def);
95
96
                        if (null === $class) {
97
                            return [];
98
                        }
99
                    }
100
                    $types = self::getTypes(new \ReflectionMethod($class ?? $def, $definition[1]));
101
102
                    goto resolve_types;
103
                }
104
            }
105
106
            return $allTypes ? ['array'] : [];
107
        }
108
109
        if (\is_callable($definition)) {
110
            $types = self::getTypes(Callback::toReflection($definition));
111
        } elseif (\is_string($definition)) {
112
            if (!(\class_exists($definition) || \interface_exists($definition))) {
113
                return $allTypes ? ['string'] : $types;
114
            }
115
116
            $types[] = $definition;
117
        } elseif (\is_object($definition)) {
118
            if ($definition instanceof \stdClass) {
119
                return $allTypes ? ['object'] : $types;
120
            }
121
122
            $types[] = \get_class($definition);
123
        } elseif (\is_array($definition)) {
124
            if (null !== $container && 2 === \count($definition, \COUNT_RECURSIVE)) {
125
                if ($definition[0] instanceof Definitions\Reference) {
126
                    $types = self::getTypes(new \ReflectionMethod($container->definition((string) $definition[0])->getEntity(), $definition[1]));
127
                } elseif ($definition[0] instanceof Expr\BinaryOp\Coalesce) {
128
                    $types = self::getTypes(new \ReflectionMethod($container->definition($definition[0]->left->dim->value)->getEntity(), $definition[1]));
129
                }
130
            } else {
131
                return $allTypes ? ['array'] : [];
132
            }
133
        }
134
135
        resolve_types:
136
        foreach (($types ?? []) as $type) {
137
            $autowired[] = $type;
138
139
            foreach (\class_implements($type) ?: [] as $interface) {
140
                $autowired[] = $interface;
141
            }
142
143
            foreach (\class_parents($type) ?: [] as $parent) {
144
                $autowired[] = $parent;
145
            }
146
        }
147
148
        return $autowired;
149
    }
150
151
    /**
152
     * Resolves arguments for callable.
153
     *
154
     * @param array<int|string,mixed> $args
155
     *
156
     * @return array<int,mixed>
157
     */
158
    public function autowireArguments(\ReflectionFunctionAbstract $function, array $args = []): array
159
    {
160
        $resolvedParameters = [];
161
        $nullValuesFound = 0;
162
        $args = $this->resolveArguments($args); // Resolves provided arguments.
163
164
        foreach ($function->getParameters() as $offset => $parameter) {
165
            $position = 0 === $nullValuesFound ? $offset : $parameter->name;
166
            $resolved = $args[$offset] ?? $args[$parameter->name] ?? null;
167
            $types = self::getTypes($parameter);
168
169
            if (\PHP_VERSION_ID >= 80100 && (\count($types) > 1 && \is_subclass_of($enumType = $types[0], \BackedEnum::class))) {
0 ignored issues
show
Bug introduced by
The type BackedEnum 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...
170
                if (null === ($resolved = $resolved ?? $providedParameters[$enumType] ?? null)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $providedParameters seems to never exist and therefore isset should always be false.
Loading history...
171
                    throw new ContainerResolutionException(\sprintf('Missing parameter %s.', Reflection::toString($parameter)));
172
                }
173
                $resolvedParameters[$position] = $enumType::from($resolved);
174
175
                continue;
176
            }
177
178
            if (null === ($resolved = $resolved ?? $this->autowireArgument($parameter, $types, $args))) {
179
                if ($parameter->isDefaultValueAvailable()) {
180
                    if (\PHP_MAJOR_VERSION < 8) {
181
                        $resolvedParameters[$position] = Reflection::getParameterDefaultValue($parameter);
182
                    } else {
183
                        ++$nullValuesFound;
184
                    }
185
                } elseif (!$parameter->isVariadic()) {
186
                    $resolvedParameters[$position] = self::getParameterDefaultValue($parameter, $types);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $resolvedParameters[$position] is correct as self::getParameterDefaultValue($parameter, $types) targeting Rade\DI\Resolver::getParameterDefaultValue() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
187
                }
188
189
                continue;
190
            }
191
192
            if ($parameter->isVariadic() && \is_array($resolved)) {
193
                $resolvedParameters = \array_merge($resolvedParameters, $resolved);
194
195
                continue;
196
            }
197
198
            $resolvedParameters[$position] = $resolved;
199
        }
200
201
        return $resolvedParameters;
202
    }
203
204
    /**
205
     * Resolve a service definition, class string, invocable object or callable
206
     * using autowiring.
207
     *
208
     * @param string|callable|object  $callback
209
     * @param array<int|string,mixed> $args
210
     *
211
     * @throws ContainerResolutionException|\ReflectionException if unresolvable
212
     *
213
     * @return mixed
214
     */
215
    public function resolve($callback, array $args = [])
216
    {
217
        if ($callback instanceof Definitions\Statement) {
218
            if (Services\ServiceLocator::class == ($value = $callback->getValue())) {
219
                $services = [];
220
221
                foreach (($callback->getArguments() ?: $args) as $name => $service) {
222
                    $services += $this->resolveServiceSubscriber($name, (string) $service);
223
                }
224
225
                $resolved = null === $this->builder ? new Services\ServiceLocator($services) : $this->builder->new('\\' . Services\ServiceLocator::class, [$services]);
226
            } else {
227
                $resolved = $this->resolve($value, $callback->getArguments() ?: $args);
228
229
                if ($callback->isClosureWrappable()) {
230
                    $resolved = null === $this->builder ? fn () => $resolved : new Expr\ArrowFunction(['expr' => $resolved]);
231
                }
232
            }
233
        } elseif ($callback instanceof Definitions\Reference) {
234
            $resolved = $this->resolveReference((string) $callback);
235
236
            if (\is_callable($resolved) || (\is_array($resolved) && 2 === \count($resolved, \COUNT_RECURSIVE))) {
237
                $resolved = $this->resolveCallable($resolved, $args);
238
            } else {
239
                $callback = $resolved;
240
            }
241
        } elseif ($callback instanceof Definitions\ValueDefinition) {
242
            $resolved = $callback->getEntity();
243
        } elseif ($callback instanceof Definitions\TaggedLocator) {
244
            $resolved = $this->resolve($callback->resolve($this->container));
245
        } elseif ($callback instanceof Builder\PhpLiteral) {
246
            $expression = $this->literalCache[\spl_object_id($callback)] ??= $callback->resolve($this)[0];
247
            $resolved = $expression instanceof Stmt\Expression ? $expression->expr : $expression;
248
        } elseif (\is_string($callback)) {
249
            if (\str_contains($callback, '%')) {
250
                $callback = $this->container->parameter($callback);
251
            }
252
253
            if (\class_exists($callback)) {
254
                return $this->resolveClass($callback, $args);
255
            }
256
257
            if (\is_callable($callback)) {
258
                $resolved = $this->resolveCallable($callback, $args);
259
            }
260
        } elseif (\is_callable($callback) || \is_array($callback)) {
261
            $resolved = $this->resolveCallable($callback, $args);
0 ignored issues
show
Bug introduced by
It seems like $callback can also be of type object; however, parameter $callback of Rade\DI\Resolver::resolveCallable() does only seem to accept array<integer,mixed>|callable, 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

261
            $resolved = $this->resolveCallable(/** @scrutinizer ignore-type */ $callback, $args);
Loading history...
262
        }
263
264
        return $resolved ?? (null === $this->builder ? $callback : $this->builder->val($callback));
265
    }
266
267
    /**
268
     * Resolves callables and array like callables.
269
     *
270
     * @param callable|array<int,mixed> $callback
271
     * @param array<int|string,mixed>   $arguments
272
     *
273
     * @throws \ReflectionException if $callback is not a real callable
274
     *
275
     * @return mixed
276
     */
277
    public function resolveCallable($callback, array $arguments = [])
278
    {
279
        if (\is_array($callback)) {
280
            if (2 === \count($callback, \COUNT_RECURSIVE) && \is_string($callback[1])) {
281
                $callback[0] = $this->resolve($callback[0]);
282
283
                if ($callback[0] instanceof Expr\BinaryOp\Coalesce) {
284
                    $class = self::getDefinitionClass($this->container->definition($callback[0]->left->dim->value));
285
286
                    if (null !== $class) {
287
                        $type = [$class, $callback[1]];
288
                    }
289
                } elseif ($callback[0] instanceof Expr\New_) {
290
                    $type = [(string) $callback[0]->class, $callback[1]];
291
                }
292
293
                if (isset($type) || \is_callable($callback)) {
294
                    goto create_callable;
295
                }
296
            }
297
298
            $callback = $this->resolveArguments($callback);
299
300
            return null === $this->builder ? $callback : $this->builder->val($callback);
301
        }
302
303
        create_callable:
304
        $args = $this->autowireArguments($ref = Callback::toReflection($type ?? $callback), $arguments);
305
306
        if ($ref instanceof \ReflectionFunction) {
307
            return null === $this->builder ? $ref->invokeArgs($args) : $this->builder->funcCall($callback, $args);
0 ignored issues
show
Bug introduced by
$callback of type array<integer,mixed>|callable is incompatible with the type PhpParser\Node\Expr|PhpParser\Node\Name|string expected by parameter $name of PhpParser\BuilderFactory::funcCall(). ( Ignorable by Annotation )

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

307
            return null === $this->builder ? $ref->invokeArgs($args) : $this->builder->funcCall(/** @scrutinizer ignore-type */ $callback, $args);
Loading history...
308
        }
309
310
        if ($ref->isStatic()) {
311
            $className = \is_array($callback) ? $callback[0] : $ref->getDeclaringClass()->getName();
312
313
            return null === $this->builder ? $ref->invokeArgs(null, $args) : $this->builder->staticCall($className, $ref->getName(), $args);
314
        }
315
316
        return null === $this->builder ? $callback(...$args) : $this->builder->methodCall($callback[0], $ref->getName(), $args);
317
    }
318
319
    /**
320
     * @param array<int|string,mixed> $args
321
     *
322
     * @throws ContainerResolutionException|\ReflectionException if class string unresolvable
323
     */
324
    public function resolveClass(string $class, array $args = []): object
325
    {
326
        /** @var class-string $class */
327
        $reflection = new \ReflectionClass($class);
328
329
        if ($reflection->isAbstract() || !$reflection->isInstantiable()) {
330
            throw new ContainerResolutionException(\sprintf('Class %s is an abstract type or instantiable.', $class));
331
        }
332
333
        if (null === $constructor = $reflection->getConstructor()) {
334
            if (!empty($args)) {
335
                throw new ContainerResolutionException(\sprintf('Unable to pass arguments, class "%s" has no constructor.', $class));
336
            }
337
338
            $service = null === $this->builder ? $reflection->newInstanceWithoutConstructor() : $this->builder->new($class);
339
        } else {
340
            $args = $this->autowireArguments($constructor, $args);
341
            $service = null === $this->builder ? $reflection->newInstanceArgs($args) : $this->builder->new($class, $args);
342
        }
343
344
        if ($reflection->implementsInterface(Injector\InjectableInterface::class)) {
345
            return Injector\Injectable::getProperties($this, $service, $reflection);
346
        }
347
348
        return $service;
349
    }
350
351
    /**
352
     * @param array<int|string,mixed> $arguments
353
     *
354
     * @return array<int|string,mixed>
355
     */
356
    public function resolveArguments(array $arguments = []): array
357
    {
358
        foreach ($arguments as $key => $value) {
359
            if ($value instanceof \stdClass) {
360
                $resolved = null === $this->builder ? $value : new Expr\Cast\Object_($this->builder->val($this->resolveArguments((array) $value)));
361
            } elseif (\is_array($value)) {
362
                $resolved = $this->resolveArguments($value);
363
            } elseif (\is_int($value)) {
364
                $resolved = null === $this->builder ? $value : new Scalar\LNumber($value);
365
            } elseif (\is_float($value)) {
366
                $resolved = null === $this->builder ? (int) $value : new Scalar\DNumber($value);
367
            } elseif (\is_numeric($value)) {
368
                $resolved = null === $this->builder ? (int) $value : Scalar\LNumber::fromString($value);
369
            } elseif (\is_string($value)) {
370
                if (\str_contains($value, '%')) {
371
                    $value = $this->container->parameter($value);
372
                }
373
374
                $resolved = null === $this->builder ? $value : $this->builder->val($value);
375
            } else {
376
                $resolved = $this->resolve($value);
377
            }
378
379
            $arguments[$key] = $resolved;
380
        }
381
382
        return $arguments;
383
    }
384
385
    /**
386
     * Resolves service by type.
387
     *
388
     * @param string $id A class or an interface name
389
     *
390
     * @return mixed
391
     */
392
    public function get(string $id, bool $single = false)
393
    {
394
        if (\is_subclass_of($id, ServiceSubscriberInterface::class)) {
395
            static $services = [];
396
397
            foreach ($id::getSubscribedServices() as $name => $service) {
398
                $services += $this->resolveServiceSubscriber($name, $service);
399
            }
400
401
            if (null === $builder = $this->builder) {
402
                return new Services\ServiceLocator($services);
403
            }
404
405
            return $builder->new('\\' . Services\ServiceLocator::class, [$services]);
406
        }
407
408
        if (!$this->strict) {
409
            return $this->container->get($id, $single ? $this->container::EXCEPTION_ON_MULTIPLE_SERVICE : $this->container::IGNORE_MULTIPLE_SERVICE);
410
        }
411
412
        if ($this->container->typed($id)) {
413
            return $this->container->autowired($id, $single);
414
        }
415
416
        throw new NotFoundServiceException(\sprintf('Service of type "%s" not found. Check class name because it cannot be found.', $id));
417
    }
418
419
    /**
420
     * Gets the PHP's parser builder.
421
     */
422
    public function getBuilder(): ?BuilderFactory
423
    {
424
        return $this->builder;
425
    }
426
427
    /**
428
     * @return mixed
429
     */
430
    private function resolveReference(string $reference)
431
    {
432
        if ('?' === $reference[0]) {
433
            $invalidBehavior = $this->container::EXCEPTION_ON_MULTIPLE_SERVICE;
434
            $reference = \substr($reference, 1);
435
436
            if ($arrayLike = \str_ends_with('[]', $reference)) {
437
                $reference = \substr($reference, 0, -2);
438
                $invalidBehavior = $this->container::IGNORE_MULTIPLE_SERVICE;
439
            }
440
441
            if ($this->container->has($reference) || $this->container->typed($reference)) {
442
                return $this->container->get($reference, $invalidBehavior);
443
            }
444
445
            return $arrayLike ? [] : null;
446
        }
447
448
        if ('[]' === \substr($reference, -2)) {
449
            return $this->container->get(\substr($reference, 0, -2), $this->container::IGNORE_MULTIPLE_SERVICE);
450
        }
451
452
        return $this->container->get($reference);
453
    }
454
455
    /**
456
     * Resolves services for ServiceLocator.
457
     *
458
     * @param int|string $id
459
     *
460
     * @return (\Closure|array|mixed|null)[]
461
     */
462
    private function resolveServiceSubscriber($id, string $value): array
463
    {
464
        if ('?' === $value[0]) {
465
            $arrayLike = \str_ends_with($value = \substr($value, 1), '[]');
466
467
            if (\is_int($id)) {
468
                $id = $arrayLike ? \substr($value, 0, -2) : $value;
469
            }
470
471
            return ($this->container->has($id) || $this->container->typed($id)) ? $this->resolveServiceSubscriber($id, $value) : [$id => $arrayLike ? [] : null];
472
        }
473
474
        $service = function () use ($value) {
475
            if ('[]' === \substr($value, -2)) {
476
                $service = $this->container->get(\substr($value, 0, -2), $this->container::IGNORE_MULTIPLE_SERVICE);
477
478
                return \is_array($service) ? $service : [$service];
479
            }
480
481
            return $this->container->get($value);
482
        };
483
484
        if (null !== $this->builder) {
485
            if ($this->container->has($value)) {
486
                $returnType = $this->container->definition($value)->getTypes()[0] ?? (\class_exists($id) || \interface_exists($id) ? $id : null);
487
            } elseif ('[]' !== \substr($value, -2)) {
488
                $returnType = 'array';
489
            }
490
491
            $service = new Expr\ArrowFunction(['expr' => $this->builder->val($service()), 'returnType' => $returnType ?? null]);
492
        }
493
494
        return [\is_int($id) ? \rtrim($value, '[]') : $id => $service];
495
    }
496
497
    /**
498
     * Resolves missing argument using autowiring.
499
     *
500
     * @param array<int|string,mixed> $providedParameters
501
     * @param array<int,string>       $types
502
     *
503
     * @throws ContainerResolutionException
504
     *
505
     * @return mixed
506
     */
507
    private function autowireArgument(\ReflectionParameter $parameter, array $types, array $providedParameters)
508
    {
509
        foreach ($types as $typeName) {
510
            if (!Reflection::isBuiltinType($typeName)) {
511
                try {
512
                    return $providedParameters[$typeName] ?? $this->get($typeName, !$parameter->isVariadic());
513
                } catch (NotFoundServiceException $e) {
514
                    // Ignore this exception ...
515
                } catch (ContainerResolutionException $e) {
516
                    $errorException = new ContainerResolutionException(\sprintf("{$e->getMessage()} (needed by %s)", Reflection::toString($parameter)));
517
                }
518
519
                if (
520
                    ServiceProviderInterface::class === $typeName &&
521
                    null !== $class = $parameter->getDeclaringClass()
522
                ) {
523
                    if (!$class->implementsInterface(ServiceSubscriberInterface::class)) {
524
                        throw new ContainerResolutionException(\sprintf(
525
                            'Service of type %s needs parent class %s to implement %s.',
526
                            $typeName,
527
                            $class->getName(),
528
                            ServiceSubscriberInterface::class
529
                        ));
530
                    }
531
532
                    return $this->get($class->getName());
533
                }
534
            }
535
536
            if (\PHP_MAJOR_VERSION >= 8 && $attributes = $parameter->getAttributes()) {
537
                foreach ($attributes as $attribute) {
538
                    if (Attribute\Inject::class === $attribute->getName()) {
539
                        if (null === $attrName = $attribute->getArguments()[0] ?? null) {
540
                            throw new ContainerResolutionException(\sprintf('Using the Inject attribute on parameter %s requires a value to be set.', $parameter->getName()));
541
                        }
542
543
                        if ($arrayLike = \str_ends_with($attrName, '[]')) {
544
                            $attrName = \substr($attrName, 0, -2);
545
                        }
546
547
                        try {
548
                            return $this->get($attrName, !$arrayLike);
549
                        } catch (NotFoundServiceException $e) {
550
                            // Ignore this exception ...
551
                        }
552
                    }
553
                }
554
            }
555
556
            if (
557
                ($method = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod
558
                && \preg_match('#@param[ \t]+([\w\\\\]+)(?:\[\])?[ \t]+\$' . $parameter->name . '#', (string) $method->getDocComment(), $m)
559
                && ($itemType = Reflection::expandClassName($m[1], $method->getDeclaringClass()))
560
                && (\class_exists($itemType) || \interface_exists($itemType))
561
            ) {
562
                try {
563
                    if (\in_array($typeName, ['array', 'iterable'], true)) {
564
                        return $this->get($itemType);
565
                    }
566
567
                    if ('object' === $typeName || \is_subclass_of($itemType, $typeName)) {
568
                        return $this->get($itemType, true);
569
                    }
570
                } catch (NotFoundServiceException $e) {
571
                    // Ignore this exception ...
572
                }
573
            }
574
575
            if (isset($errorException)) {
576
                throw $errorException;
577
            }
578
        }
579
580
        return null;
581
    }
582
583
    /**
584
     * Returns an associated type to the given parameter if available.
585
     *
586
     * @param \ReflectionParameter|\ReflectionFunctionAbstract $reflection
587
     *
588
     * @return array<int,string>
589
     */
590
    private static function getTypes(\Reflector $reflection): array
591
    {
592
        if ($reflection instanceof \ReflectionParameter) {
593
            $type = $reflection->getType();
594
        } elseif ($reflection instanceof \ReflectionFunctionAbstract) {
595
            $type = $reflection->getReturnType() ?? (PHP_VERSION_ID >= 80100 ? $reflection->getTentativeReturnType() : null);
0 ignored issues
show
Bug introduced by
The method getTentativeReturnType() does not exist on ReflectionFunctionAbstract. ( Ignorable by Annotation )

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

595
            $type = $reflection->getReturnType() ?? (PHP_VERSION_ID >= 80100 ? $reflection->/** @scrutinizer ignore-call */ getTentativeReturnType() : null);

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...
596
        }
597
598
        if (!isset($type)) {
599
            return [];
600
        }
601
602
        $resolver = static function (\ReflectionNamedType $rName) use ($reflection): string {
603
            $function = $reflection instanceof \ReflectionParameter ? $reflection->getDeclaringFunction() : $reflection;
604
605
            if ($function instanceof \ReflectionMethod) {
606
                $lcName = \strtolower($rName->getName());
607
608
                if ('self' === $lcName || 'static' === $lcName) {
609
                    return $function->getDeclaringClass()->name;
610
                }
611
612
                if ('parent' === $lcName) {
613
                    return $function->getDeclaringClass()->getParentClass()->name;
614
                }
615
            }
616
617
            return $rName->getName();
618
        };
619
620
        if (!$type instanceof \ReflectionNamedType) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $type does not seem to be defined for all execution paths leading up to this point.
Loading history...
621
            return \array_map($resolver, $type->getTypes());
622
        }
623
624
        return [$resolver($type)];
625
    }
626
627
    private static function getDefinitionClass(Definitions\DefinitionInterface $def): ?string
628
    {
629
        if (!\class_exists($class = $def->getEntity())) {
630
            if ($def instanceof Definitions\TypedDefinitionInterface) {
631
                foreach ($def->getTypes() as $typed) {
632
                    if (\class_exists($typed)) {
633
                        return $typed;
634
                    }
635
                }
636
            }
637
638
            return null;
639
        }
640
641
        return $class;
642
    }
643
644
    /**
645
     * Get the parameter's allowed null else error.
646
     *
647
     * @throws \ReflectionException|ContainerResolutionException
648
     *
649
     * @return null
650
     */
651
    private static function getParameterDefaultValue(\ReflectionParameter $parameter, array $types)
652
    {
653
        if ($parameter->isOptional() || $parameter->allowsNull()) {
654
            return null;
655
        }
656
657
        $errorDescription = 'Parameter ' . Reflection::toString($parameter);
658
659
        if ('' === ($typedHint = \implode('|', $types))) {
660
            $errorDescription .= ' has no type hint or default value.';
661
        } elseif (\str_contains($typedHint, '|')) {
662
            $errorDescription .= ' has multiple type-hints ("' . $typedHint . '").';
663
        } elseif (\class_exists($typedHint)) {
664
            $errorDescription .= ' has an unresolved class-based type-hint ("' . $typedHint . '").';
665
        } elseif (\interface_exists($typedHint)) {
666
            $errorDescription .= ' has an unresolved interface-based type-hint ("' . $typedHint . '").';
667
        } else {
668
            $errorDescription .= ' has a type-hint ("' . $typedHint  . '") that cannot be resolved, perhaps a you forgot to set it up?';
669
        }
670
671
        throw new ContainerResolutionException($errorDescription);
672
    }
673
}
674