Passed
Branch main (8d1023)
by Chema
01:53
created

DependencyResolver::interfaceExists()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

252
            return $reflection->newInstanceArgs(/** @scrutinizer ignore-type */ $innerDependencies);
Loading history...
253
        } finally {
254 16
            unset($this->resolvingStack[$className]);
255
        }
256
    }
257
}
258