Test Failed
Branch main (917e6f)
by Chema
25:55 queued 22:47
created

DependencyResolver   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 195
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 70
c 2
b 0
f 0
dl 0
loc 195
rs 10
ccs 61
cts 61
cp 1
wmc 29

11 Methods

Rating   Name   Duplication   Size   Complexity  
A resolveReflectionClass() 0 21 4
A __construct() 0 3 1
A resolveDependencies() 0 13 2
A getConstructor() 0 8 2
A resolveInnerDependencies() 0 20 3
A checkCircularDependency() 0 6 2
A resolveClass() 0 22 4
A isScalar() 0 4 2
A getParametersToResolve() 0 12 3
A checkInvalidArgumentParam() 0 14 4
A resolveDependenciesRecursively() 0 14 2
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
    /**
33
     * @param array<class-string,class-string|callable|object> $bindings
34
     */
35 31
    public function __construct(
36
        private array $bindings = [],
37
    ) {
38 31
    }
39
40 31
    /**
41
     * @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...
42 31
     *
43
     * @return list<mixed>
44 26
     */
45
    public function resolveDependencies(string|Closure $toResolve): array
46
    {
47 25
        /** @var list<mixed> $dependencies */
48
        $dependencies = [];
49
50
        $parameters = $this->getParametersToResolve($toResolve);
51
52
        foreach ($parameters as $parameter) {
53
            /** @psalm-suppress MixedAssignment */
54
            $dependencies[] = $this->resolveDependenciesRecursively($parameter);
55 31
        }
56
57 31
        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...
58 19
    }
59 19
60 19
    /**
61 3
     * @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...
62
     *
63 16
     * @return list<ReflectionParameter>
64
     */
65
    private function getParametersToResolve(Closure|string $toResolve): array
66 12
    {
67 12
        if (is_string($toResolve)) {
68
            $constructor = $this->getConstructor($toResolve);
69
            if (!$constructor) {
70 26
                return [];
71
            }
72 26
            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...
73
        }
74
75 22
        $reflection = new ReflectionFunction($toResolve);
76
        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...
77
    }
78 22
79 22
    /**
80 14
     * @param class-string $className
81
     */
82
    private function getConstructor(string $className): ?ReflectionMethod
83 19
    {
84
        if (!isset($this->constructorCache[$className])) {
85
            $reflection = new ReflectionClass($className);
86 26
            $this->constructorCache[$className] = $reflection->getConstructor();
87
        }
88 26
89 2
        return $this->constructorCache[$className];
90
    }
91
92
    private function resolveDependenciesRecursively(ReflectionParameter $parameter): mixed
93 24
    {
94 24
        $this->checkInvalidArgumentParam($parameter);
95
96 24
        /** @var ReflectionNamedType $paramType */
97
        $paramType = $parameter->getType();
98 2
99 2
        /** @var class-string $paramTypeName */
100
        $paramTypeName = $paramType->getName();
101
        if ($parameter->isDefaultValueAvailable()) {
102
            return $parameter->getDefaultValue();
103 24
        }
104
105 24
        return $this->resolveClass($paramTypeName);
106 24
    }
107
108
    private function checkInvalidArgumentParam(ReflectionParameter $parameter): void
109
    {
110
        if (!$parameter->hasType()) {
111
            throw DependencyInvalidArgumentException::noParameterTypeFor($parameter->getName());
112 19
        }
113
114
        /** @var ReflectionNamedType $paramType */
115 19
        $paramType = $parameter->getType();
116 19
        $paramTypeName = $paramType->getName();
117 1
118
        if ($this->isScalar($paramTypeName) && !$parameter->isDefaultValueAvailable()) {
119
            /** @var ReflectionClass $reflectionClass */
120 18
            $reflectionClass = $parameter->getDeclaringClass();
121 3
            throw DependencyInvalidArgumentException::unableToResolve($paramTypeName, $reflectionClass->getName());
122
        }
123
    }
124 15
125 13
    private function isScalar(string $paramTypeName): bool
126 13
    {
127 2
        return !class_exists($paramTypeName)
128
            && !interface_exists($paramTypeName);
129
    }
130 11
131
    /**
132
     * @param class-string $paramTypeName
133
     */
134
    private function resolveClass(string $paramTypeName): mixed
135
    {
136 15
        /** @var mixed $bindClass */
137
        $bindClass = $this->bindings[$paramTypeName] ?? null;
138 15
        if (is_callable($bindClass)) {
139
            return $bindClass();
140 15
        }
141 11
142
        if (is_object($bindClass)) {
143
            return $bindClass;
144
        }
145 5
146
        $this->checkCircularDependency($paramTypeName);
147 5
148
        $reflection = $this->resolveReflectionClass($paramTypeName);
149 3
        // Use the concrete class name for caching, not the original parameter type
150
        $constructor = $this->getConstructor($reflection->getName());
151
        if ($constructor === null) {
152 2
            return $reflection->newInstance();
153
        }
154
155 11
        return $this->resolveInnerDependencies($constructor, $reflection);
156
    }
157
158 11
    /**
159
     * @param class-string $className
160 11
     */
161 11
    private function checkCircularDependency(string $className): void
162 11
    {
163
        if (isset($this->resolvingStack[$className])) {
164 11
            $chain = array_keys($this->resolvingStack);
165
            $chain[] = $className;
166
            throw CircularDependencyException::create($chain);
167
        }
168 11
    }
169
170
    /**
171
     * @param class-string $paramTypeName
172
     */
173
    private function resolveReflectionClass(string $paramTypeName): ReflectionClass
174
    {
175
        if (!isset($this->reflectionCache[$paramTypeName])) {
176
            $reflection = new ReflectionClass($paramTypeName);
177
178
            if (!$reflection->isInstantiable()) {
179
                /** @var mixed $concreteClass */
180
                $concreteClass = $this->bindings[$reflection->getName()] ?? null;
181
182
                if ($concreteClass !== null) {
183
                    /** @var class-string $concreteClass */
184
                    $reflection = new ReflectionClass($concreteClass);
185
                } else {
186
                    throw DependencyNotFoundException::mapNotFoundForClassName($reflection->getName());
187
                }
188
            }
189
190
            $this->reflectionCache[$paramTypeName] = $reflection;
191
        }
192
193
        return $this->reflectionCache[$paramTypeName];
194
    }
195
196
    private function resolveInnerDependencies(ReflectionMethod $constructor, ReflectionClass $reflection): object
197
    {
198
        $className = $reflection->getName();
199
        $this->resolvingStack[$className] = true;
200
201
        try {
202
            /** @var list<mixed> $innerDependencies */
203
            $innerDependencies = [];
204
205
            foreach ($constructor->getParameters() as $constructorParameter) {
206
                $paramType = $constructorParameter->getType();
207
                if ($paramType) {
208
                    /** @psalm-suppress MixedAssignment */
209
                    $innerDependencies[] = $this->resolveDependenciesRecursively($constructorParameter);
210
                }
211
            }
212
213
            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

213
            return $reflection->newInstanceArgs(/** @scrutinizer ignore-type */ $innerDependencies);
Loading history...
214
        } finally {
215
            unset($this->resolvingStack[$className]);
216
        }
217
    }
218
}
219