Passed
Push — main ( 08d8b4...a03e47 )
by Chema
57s queued 14s
created

resolveDependenciesRecursively()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 14
rs 10
ccs 7
cts 7
cp 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Container;
6
7
use Closure;
8
use Gacela\Container\Exception\DependencyInvalidArgumentException;
9
use Gacela\Container\Exception\DependencyNotFoundException;
10
use ReflectionClass;
11
use ReflectionFunction;
12
use ReflectionMethod;
13
use ReflectionNamedType;
14
use ReflectionParameter;
15
16
use function is_callable;
17
use function is_object;
18
use function is_string;
19
20
final class DependencyResolver
21
{
22
    /**
23
     * @param array<class-string,class-string|callable|object> $bindings
24
     */
25 31
    public function __construct(
26
        private array $bindings = [],
27
    ) {
28 31
    }
29
30
    /**
31
     * @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...
32
     *
33
     * @return list<mixed>
34
     */
35 31
    public function resolveDependencies(string|Closure $toResolve): array
36
    {
37
        /** @var list<mixed> $dependencies */
38 31
        $dependencies = [];
39
40 31
        $parameters = $this->getParametersToResolve($toResolve);
41
42 31
        foreach ($parameters as $parameter) {
43
            /** @psalm-suppress MixedAssignment */
44 26
            $dependencies[] = $this->resolveDependenciesRecursively($parameter);
45
        }
46
47 25
        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...
48
    }
49
50
    /**
51
     * @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...
52
     *
53
     * @return list<ReflectionParameter>
54
     */
55 31
    private function getParametersToResolve(Closure|string $toResolve): array
56
    {
57 31
        if (is_string($toResolve)) {
58 19
            $reflection = new ReflectionClass($toResolve);
59 19
            $constructor = $reflection->getConstructor();
60 19
            if (!$constructor) {
61 3
                return [];
62
            }
63 16
            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...
64
        }
65
66 12
        $reflection = new ReflectionFunction($toResolve);
67 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...
68
    }
69
70 26
    private function resolveDependenciesRecursively(ReflectionParameter $parameter): mixed
71
    {
72 26
        $this->checkInvalidArgumentParam($parameter);
73
74
        /** @var ReflectionNamedType $paramType */
75 22
        $paramType = $parameter->getType();
76
77
        /** @var class-string $paramTypeName */
78 22
        $paramTypeName = $paramType->getName();
79 22
        if ($parameter->isDefaultValueAvailable()) {
80 14
            return $parameter->getDefaultValue();
81
        }
82
83 19
        return $this->resolveClass($paramTypeName);
84
    }
85
86 26
    private function checkInvalidArgumentParam(ReflectionParameter $parameter): void
87
    {
88 26
        if (!$parameter->hasType()) {
89 2
            throw DependencyInvalidArgumentException::noParameterTypeFor($parameter->getName());
90
        }
91
92
        /** @var ReflectionNamedType $paramType */
93 24
        $paramType = $parameter->getType();
94 24
        $paramTypeName = $paramType->getName();
95
96 24
        if ($this->isScalar($paramTypeName) && !$parameter->isDefaultValueAvailable()) {
97
            /** @var ReflectionClass $reflectionClass */
98 2
            $reflectionClass = $parameter->getDeclaringClass();
99 2
            throw DependencyInvalidArgumentException::unableToResolve($paramTypeName, $reflectionClass->getName());
100
        }
101
    }
102
103 24
    private function isScalar(string $paramTypeName): bool
104
    {
105 24
        return !class_exists($paramTypeName)
106 24
            && !interface_exists($paramTypeName);
107
    }
108
109
    /**
110
     * @param class-string $paramTypeName
111
     */
112 19
    private function resolveClass(string $paramTypeName): mixed
113
    {
114
        /** @var mixed $bindClass */
115 19
        $bindClass = $this->bindings[$paramTypeName] ?? null;
116 19
        if (is_callable($bindClass)) {
117 1
            return $bindClass();
118
        }
119
120 18
        if (is_object($bindClass)) {
121 3
            return $bindClass;
122
        }
123
124 15
        $reflection = $this->resolveReflectionClass($paramTypeName);
125 13
        $constructor = $reflection->getConstructor();
126 13
        if ($constructor === null) {
127 2
            return $reflection->newInstance();
128
        }
129
130 11
        return $this->resolveInnerDependencies($constructor, $reflection);
131
    }
132
133
    /**
134
     * @param class-string $paramTypeName
135
     */
136 15
    private function resolveReflectionClass(string $paramTypeName): ReflectionClass
137
    {
138 15
        $reflection = new ReflectionClass($paramTypeName);
139
140 15
        if ($reflection->isInstantiable()) {
141 11
            return $reflection;
142
        }
143
144
        /** @var mixed $concreteClass */
145 5
        $concreteClass = $this->bindings[$reflection->getName()] ?? null;
146
147 5
        if ($concreteClass !== null) {
148
            /** @var class-string $concreteClass */
149 3
            return new ReflectionClass($concreteClass);
150
        }
151
152 2
        throw DependencyNotFoundException::mapNotFoundForClassName($reflection->getName());
153
    }
154
155 11
    private function resolveInnerDependencies(ReflectionMethod $constructor, ReflectionClass $reflection): object
156
    {
157
        /** @var list<mixed> $innerDependencies */
158 11
        $innerDependencies = [];
159
160 11
        foreach ($constructor->getParameters() as $constructorParameter) {
161 11
            $paramType = $constructorParameter->getType();
162 11
            if ($paramType) {
163
                /** @psalm-suppress MixedAssignment */
164 11
                $innerDependencies[] = $this->resolveDependenciesRecursively($constructorParameter);
165
            }
166
        }
167
168 11
        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

168
        return $reflection->newInstanceArgs(/** @scrutinizer ignore-type */ $innerDependencies);
Loading history...
169
    }
170
}
171