Passed
Branch main (61b4ec)
by Chema
02:26
created

DependencyResolver   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 132
Duplicated Lines 0 %

Test Coverage

Coverage 96.36%

Importance

Changes 0
Metric Value
wmc 23
eloc 46
dl 0
loc 132
ccs 53
cts 55
cp 0.9636
rs 10
c 0
b 0
f 0

8 Methods

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

148
        return $reflection->newInstanceArgs(/** @scrutinizer ignore-type */ $innerDependencies);
Loading history...
149
    }
150
}
151