Passed
Push — main ( ad40d6...a04da5 )
by Chema
02:25
created

resolveDependenciesRecursively()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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

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