DependencyResolver::resolveClass()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 1
dl 0
loc 22
ccs 1
cts 1
cp 1
crap 4
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Container;
6
7
use Closure;
8
use Gacela\Container\Exception\CircularDependencyException;
9
use Gacela\Container\Exception\DependencyInvalidArgumentException;
10
use Gacela\Container\Exception\DependencyNotFoundException;
11
use ReflectionClass;
12
use ReflectionFunction;
13
use ReflectionMethod;
14
use ReflectionNamedType;
15
use ReflectionParameter;
16
17
use function is_callable;
18
use function is_object;
19
use function is_string;
20
21
final class DependencyResolver
22
{
23
    /** @var array<class-string, ReflectionClass> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, ReflectionClass> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, ReflectionClass>.
Loading history...
24
    private array $reflectionCache = [];
25 31
26
    /** @var array<class-string, ?ReflectionMethod> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, ?ReflectionMethod> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, ?ReflectionMethod>.
Loading history...
27
    private array $constructorCache = [];
28 31
29
    /** @var array<class-string, bool> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, bool> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, bool>.
Loading history...
30
    private array $resolvingStack = [];
31
32
    /** @var array<string, bool> */
33
    private array $classExistsCache = [];
34
35 31
    /** @var array<string, bool> */
36
    private array $interfaceExistsCache = [];
37
38 31
    /**
39
     * @param array<class-string,class-string|callable|object> $bindings
40 31
     */
41
    public function __construct(
42 31
        private array $bindings = [],
43
    ) {
44 26
    }
45
46
    /**
47 25
     * @param class-string|Closure $toResolve
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|Closure at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|Closure.
Loading history...
48
     *
49
     * @return list<mixed>
50
     */
51
    public function resolveDependencies(string|Closure $toResolve): array
52
    {
53
        /** @var list<mixed> $dependencies */
54
        $dependencies = [];
55 31
56
        $parameters = $this->getParametersToResolve($toResolve);
57 31
58 19
        foreach ($parameters as $parameter) {
59 19
            /** @psalm-suppress MixedAssignment */
60 19
            $dependencies[] = $this->resolveDependenciesRecursively($parameter);
61 3
        }
62
63 16
        return $dependencies;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $dependencies returns the type Gacela\Container\list which is incompatible with the type-hinted return array.
Loading history...
64
    }
65
66 12
    /**
67 12
     * @param class-string|Closure $toResolve
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|Closure at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|Closure.
Loading history...
68
     *
69
     * @return list<ReflectionParameter>
70 26
     */
71
    private function getParametersToResolve(Closure|string $toResolve): array
72 26
    {
73
        if (is_string($toResolve)) {
74
            $constructor = $this->getConstructor($toResolve);
75 22
            if (!$constructor) {
76
                return [];
77
            }
78 22
            return $constructor->getParameters();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $constructor->getParameters() returns the type ReflectionParameter[] which is incompatible with the documented return type Gacela\Container\list.
Loading history...
79 22
        }
80 14
81
        $reflection = new ReflectionFunction($toResolve);
82
        return $reflection->getParameters();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $reflection->getParameters() returns the type ReflectionParameter[] which is incompatible with the documented return type Gacela\Container\list.
Loading history...
83 19
    }
84
85
    /**
86 26
     * @param class-string $className
87
     */
88 26
    private function getConstructor(string $className): ?ReflectionMethod
89 2
    {
90
        if (!isset($this->constructorCache[$className])) {
91
            $reflection = new ReflectionClass($className);
92
            $this->constructorCache[$className] = $reflection->getConstructor();
93 24
        }
94 24
95
        return $this->constructorCache[$className];
96 24
    }
97
98 2
    private function resolveDependenciesRecursively(ReflectionParameter $parameter): mixed
99 2
    {
100
        $this->checkInvalidArgumentParam($parameter);
101
102
        /** @var ReflectionNamedType $paramType */
103 24
        $paramType = $parameter->getType();
104
105 24
        /** @var class-string $paramTypeName */
106 24
        $paramTypeName = $paramType->getName();
107
        if ($parameter->isDefaultValueAvailable()) {
108
            return $parameter->getDefaultValue();
109
        }
110
111
        return $this->resolveClass($paramTypeName);
112 19
    }
113
114
    private function checkInvalidArgumentParam(ReflectionParameter $parameter): void
115 19
    {
116 19
        if (!$parameter->hasType()) {
117 1
            $chain = $this->getResolutionChain();
118
            throw DependencyInvalidArgumentException::noParameterTypeFor($parameter->getName(), $chain);
119
        }
120 18
121 3
        /** @var ReflectionNamedType $paramType */
122
        $paramType = $parameter->getType();
123
        $paramTypeName = $paramType->getName();
124 15
125 13
        if ($this->isScalar($paramTypeName) && !$parameter->isDefaultValueAvailable()) {
126 13
            /** @var ReflectionClass $reflectionClass */
127 2
            $reflectionClass = $parameter->getDeclaringClass();
128
            $chain = $this->getResolutionChain();
129
            throw DependencyInvalidArgumentException::unableToResolve($paramTypeName, $reflectionClass->getName(), $chain);
130 11
        }
131
    }
132
133
    /**
134
     * @return list<string>
135
     */
136 15
    private function getResolutionChain(): array
137
    {
138 15
        return array_keys($this->resolvingStack);
139
    }
140 15
141 11
    private function isScalar(string $paramTypeName): bool
142
    {
143
        return !$this->classExists($paramTypeName)
144
            && !$this->interfaceExists($paramTypeName);
145 5
    }
146
147 5
    private function classExists(string $className): bool
148
    {
149 3
        if (!isset($this->classExistsCache[$className])) {
150
            $this->classExistsCache[$className] = class_exists($className);
151
        }
152 2
153
        return $this->classExistsCache[$className];
154
    }
155 11
156
    private function interfaceExists(string $interfaceName): bool
157
    {
158 11
        if (!isset($this->interfaceExistsCache[$interfaceName])) {
159
            $this->interfaceExistsCache[$interfaceName] = interface_exists($interfaceName);
160 11
        }
161 11
162 11
        return $this->interfaceExistsCache[$interfaceName];
163
    }
164 11
165
    /**
166
     * @param class-string $paramTypeName
167
     */
168 11
    private function resolveClass(string $paramTypeName): mixed
169
    {
170
        /** @var mixed $bindClass */
171
        $bindClass = $this->bindings[$paramTypeName] ?? null;
172
        if (is_callable($bindClass)) {
173
            return $bindClass();
174
        }
175
176
        if (is_object($bindClass)) {
177
            return $bindClass;
178
        }
179
180
        $this->checkCircularDependency($paramTypeName);
181
182
        $reflection = $this->resolveReflectionClass($paramTypeName);
183
        // Use the concrete class name for caching, not the original parameter type
184
        $constructor = $this->getConstructor($reflection->getName());
185
        if ($constructor === null) {
186
            return $reflection->newInstance();
187
        }
188
189
        return $this->resolveInnerDependencies($constructor, $reflection);
190
    }
191
192
    /**
193
     * @param class-string $className
194
     */
195
    private function checkCircularDependency(string $className): void
196
    {
197
        if (isset($this->resolvingStack[$className])) {
198
            $chain = array_keys($this->resolvingStack);
199
            $chain[] = $className;
200
            throw CircularDependencyException::create($chain);
201
        }
202
    }
203
204
    /**
205
     * @param class-string $paramTypeName
206
     */
207
    private function resolveReflectionClass(string $paramTypeName): ReflectionClass
208
    {
209
        if (!isset($this->reflectionCache[$paramTypeName])) {
210
            $reflection = new ReflectionClass($paramTypeName);
211
212
            if (!$reflection->isInstantiable()) {
213
                /** @var mixed $concreteClass */
214
                $concreteClass = $this->bindings[$reflection->getName()] ?? null;
215
216
                if ($concreteClass !== null) {
217
                    /** @var class-string $concreteClass */
218
                    $reflection = new ReflectionClass($concreteClass);
219
                } else {
220
                    throw DependencyNotFoundException::mapNotFoundForClassName($reflection->getName());
221
                }
222
            }
223
224
            $this->reflectionCache[$paramTypeName] = $reflection;
225
        }
226
227
        return $this->reflectionCache[$paramTypeName];
228
    }
229
230
    private function resolveInnerDependencies(ReflectionMethod $constructor, ReflectionClass $reflection): object
231
    {
232
        $className = $reflection->getName();
233
        $this->resolvingStack[$className] = true;
234
235
        try {
236
            /** @var list<mixed> $innerDependencies */
237
            $innerDependencies = [];
238
239
            foreach ($constructor->getParameters() as $constructorParameter) {
240
                $paramType = $constructorParameter->getType();
241
                if ($paramType) {
242
                    /** @psalm-suppress MixedAssignment */
243
                    $innerDependencies[] = $this->resolveDependenciesRecursively($constructorParameter);
244
                }
245
            }
246
247
            return $reflection->newInstanceArgs($innerDependencies);
0 ignored issues
show
Bug introduced by
$innerDependencies of type Gacela\Container\list is incompatible with the type array expected by parameter $args of ReflectionClass::newInstanceArgs(). ( Ignorable by Annotation )

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

247
            return $reflection->newInstanceArgs(/** @scrutinizer ignore-type */ $innerDependencies);
Loading history...
248
        } finally {
249
            unset($this->resolvingStack[$className]);
250
        }
251
    }
252
}
253