Passed
Pull Request — master (#92)
by Sergei
07:46
created

DefinitionExtractor::fromClassName()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 8
ccs 6
cts 6
cp 1
rs 10
cc 3
nc 3
nop 1
crap 3
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
        /**
71
         * @var ReflectionNamedType|ReflectionUnionType|null $type
72
         */
73 12
        $type = $parameter->getType();
74
75 12
        if ($parameter->isVariadic()) {
76 2
            return $this->createParameterDefinition($parameter);
77
        }
78
79
        // PHP 8 union type is used as type hint
80
        /** @psalm-suppress UndefinedClass, TypeDoesNotContainType */
81 11
        if ($type instanceof ReflectionUnionType) {
82
            $types = [];
83
            /** @var ReflectionNamedType $unionType */
84
            foreach ($type->getTypes() as $unionType) {
85
                if (!$unionType->isBuiltin()) {
86
                    $typeName = $unionType->getName();
87
                    if ($typeName === 'self') {
88
                        // If type name is "self", it means that called class and
89
                        // $parameter->getDeclaringClass() returned instance of `ReflectionClass`.
90
                        /** @psalm-suppress PossiblyNullReference */
91
                        $typeName = $parameter->getDeclaringClass()->getName();
92
                    }
93
94
                    $types[] = $typeName;
95
                }
96
            }
97
98
            /** @psalm-suppress MixedArgument */
99
            return new ClassDefinition(implode('|', $types), $type->allowsNull());
100
        }
101
102
        // Our parameter has a class type hint
103 11
        if ($type !== null && !$type->isBuiltin()) {
104 6
            $typeName = $type->getName();
105 6
            if ($typeName === 'self') {
106
                // If type name is "self", it means that called class and
107
                // $parameter->getDeclaringClass() returned instance of `ReflectionClass`.
108
                /** @psalm-suppress PossiblyNullReference */
109
                $typeName = $parameter->getDeclaringClass()->getName();
110
            }
111
112 6
            return new ClassDefinition($typeName, $type->allowsNull());
113
        }
114
115
        // Our parameter does not have a class type hint and either has a default value or is nullable.
116 6
        return $this->createParameterDefinition($parameter);
117
    }
118
119
    /**
120
     * @return DefinitionInterface[]
121
     * @psalm-return array<string, DefinitionInterface>
122
     */
123
    public function fromCallable(callable $callable): array
124
    {
125
        return $this->fromFunction(new ReflectionFunction(Closure::fromCallable($callable)));
126
    }
127
128 7
    private function createParameterDefinition(ReflectionParameter $parameter): ParameterDefinition
129
    {
130 7
        $definition = new ParameterDefinition($parameter);
131
132 7
        if ($parameter->isDefaultValueAvailable()) {
133 4
            $definition->setValue($parameter->getDefaultValue());
134 4
        } elseif (!$parameter->isVariadic()) {
135 2
            $definition->setValue(null);
136
        }
137
138 7
        return $definition;
139
    }
140
}
141