Passed
Pull Request — master (#92)
by Sergei
02:35
created

DefinitionExtractor::fromFunction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Factory\Definition;
6
7
use Closure;
8
use ReflectionClass;
9
use ReflectionFunction;
10
use ReflectionFunctionAbstract;
11
use ReflectionNamedType;
12
use ReflectionParameter;
13
use ReflectionUnionType;
14
use Yiisoft\Factory\Exception\NotInstantiableException;
15
16
/**
17
 * This class resolves dependencies by using class type hints.
18
 * Note that service names need not match the parameter names, parameter names are ignored
19
 *
20
 * @internal
21
 */
22
final class DefinitionExtractor
23
{
24
    private static ?self $instance = null;
25
26 1
    private function __construct()
27
    {
28 1
    }
29
30 15
    public static function getInstance(): self
31
    {
32 15
        if (self::$instance === null) {
33 1
            self::$instance = new self();
34
        }
35
36 15
        return self::$instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::instance could return the type null which is incompatible with the type-hinted return Yiisoft\Factory\Definition\DefinitionExtractor. Consider adding an additional type-check to rule them out.
Loading history...
37
    }
38
39
    /**
40
     * @psalm-param class-string $class
41
     *
42
     * @return DefinitionInterface[]
43
     * @psalm-return array<string, DefinitionInterface>
44
     */
45 15
    public function fromClassName(string $class): array
46
    {
47 15
        $reflectionClass = new ReflectionClass($class);
48 15
        if (!$reflectionClass->isInstantiable()) {
49 5
            throw new NotInstantiableException($class);
50
        }
51 15
        $constructor = $reflectionClass->getConstructor();
52 15
        return $constructor === null ? [] : $this->fromFunction($constructor);
53
    }
54
55
    /**
56
     * @return DefinitionInterface[]
57
     * @psalm-return array<string, DefinitionInterface>
58
     */
59 12
    private function fromFunction(ReflectionFunctionAbstract $reflectionFunction): array
60
    {
61 12
        $result = [];
62 12
        foreach ($reflectionFunction->getParameters() as $parameter) {
63 12
            $result[$parameter->getName()] = $this->fromParameter($parameter);
64
        }
65 12
        return $result;
66
    }
67
68 12
    private function fromParameter(ReflectionParameter $parameter): DefinitionInterface
69
    {
70 12
        $type = $parameter->getType();
71
72 12
        if ($parameter->isVariadic()) {
73 2
            return $this->createParameterDefinition($parameter);
74
        }
75
76
        // PHP 8 union type is used as type hint
77
        /** @psalm-suppress UndefinedClass, TypeDoesNotContainType */
78 11
        if ($type instanceof ReflectionUnionType) {
79
            $types = [];
80
            /** @var ReflectionNamedType $unionType */
81
            foreach ($type->getTypes() as $unionType) {
82
                if (!$unionType->isBuiltin()) {
83
                    $typeName = $unionType->getName();
84
                    if ($typeName === 'self') {
85
                        // If type name is "self", it means that called class and
86
                        // $parameter->getDeclaringClass() returned instance of `ReflectionClass`.
87
                        /** @psalm-suppress PossiblyNullReference */
88
                        $typeName = $parameter->getDeclaringClass()->getName();
89
                    }
90
91
                    $types[] = $typeName;
92
                }
93
            }
94
95
            /** @psalm-suppress MixedArgument */
96
            return new ClassDefinition(implode('|', $types), $type->allowsNull());
97
        }
98
99
        /** @var ReflectionNamedType|null $type */
100
101
        // Our parameter has a class type hint
102 11
        if ($type !== null && !$type->isBuiltin()) {
103 6
            $typeName = $type->getName();
104 6
            if ($typeName === 'self') {
105
                // If type name is "self", it means that called class and
106
                // $parameter->getDeclaringClass() returned instance of `ReflectionClass`.
107
                /** @psalm-suppress PossiblyNullReference */
108
                $typeName = $parameter->getDeclaringClass()->getName();
109
            }
110
111 6
            return new ClassDefinition($typeName, $type->allowsNull());
112
        }
113
114
        // Our parameter does not have a class type hint and either has a default value or is nullable.
115 6
        return $this->createParameterDefinition($parameter);
116
    }
117
118
    /**
119
     * @return DefinitionInterface[]
120
     * @psalm-return array<string, DefinitionInterface>
121
     */
122
    public function fromCallable(callable $callable): array
123
    {
124
        return $this->fromFunction(new ReflectionFunction(Closure::fromCallable($callable)));
125
    }
126
127 7
    private function createParameterDefinition(ReflectionParameter $parameter): ParameterDefinition
128
    {
129 7
        $definition = new ParameterDefinition($parameter);
130
131 7
        if ($parameter->isDefaultValueAvailable()) {
132 4
            $definition->setValue($parameter->getDefaultValue());
133 4
        } elseif (!$parameter->isVariadic()) {
134 2
            $definition->setValue(null);
135
        }
136
137 7
        return $definition;
138
    }
139
}
140