Passed
Push — main ( a1a461...2097df )
by Thomas
12:50
created

Resolver::resolveConstructorArgs()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 4
nop 2
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Chuck\Di;
6
7
use Closure;
8
use Conia\Chuck\Exception\ContainerException;
9
use Conia\Chuck\Exception\NotFoundException;
10
use Conia\Chuck\Registry;
11
use ReflectionClass;
12
use ReflectionFunction;
13
use ReflectionFunctionAbstract;
14
use ReflectionMethod;
15
use ReflectionNamedType;
16
use ReflectionObject;
17
use ReflectionParameter;
18
use Throwable;
19
20
class Resolver
21
{
22 162
    public function __construct(protected readonly Registry $registry)
23
    {
24 162
    }
25
26
    /** @psalm-param class-string $class */
27 103
    public function autowire(string $class, array $predefinedArgs = []): object
28
    {
29 103
        if (!$this->registry->autowire) {
30
            try {
31 2
                $this->registry->new($class, ...$predefinedArgs);
32 1
            } catch (Throwable $e) {
33 1
                throw new ContainerException(
34 1
                    "Autowiring is turned off. Tried to instantiate class '{$class}'"
35 1
                );
36
            }
37
        }
38
39 102
        $rc = new ReflectionClass($class);
40 102
        $args = $this->resolveConstructorArgs($rc, $predefinedArgs);
41
42
        try {
43 99
            return $this->resolveCallAttributes($rc->newInstance(...$args));
44 2
        } catch (Throwable $e) {
45 2
            throw new ContainerException(
46 2
                'Autowiring unresolvable: ' . $class . ' Details: ' . $e->getMessage()
47 2
            );
48
        }
49
    }
50
51 106
    public function resolveCallAttributes(object $instance): object
52
    {
53 106
        $callAttrs = (new ReflectionObject($instance))->getAttributes(Call::class);
54
55
        // See if the attribute itself has one or more Call attributes. If so,
56
        // resolve/autowire the arguments of the method it states and call it.
57 106
        foreach ($callAttrs as $callAttr) {
58 3
            $callAttr = $callAttr->newInstance();
59 3
            $methodToResolve = $callAttr->method;
60
61
            /** @psalm-var callable */
62 3
            $callable = [$instance, $methodToResolve];
63 3
            $args = $this->resolveCallableArgs($callable, $callAttr->args);
64 3
            $callable(...$args);
65
        }
66
67 106
        return $instance;
68
    }
69
70 65
    public function resolveParam(ReflectionParameter $param): mixed
71
    {
72 65
        $type = $param->getType();
73
74 65
        if ($type instanceof ReflectionNamedType) {
75
            try {
76 61
                return $this->registry->get(ltrim($type->getName(), '?'));
77 8
            } catch (NotFoundException $e) {
78 7
                if ($param->isDefaultValueAvailable()) {
79 6
                    return $param->getDefaultValue();
80
                }
81
82 3
                throw $e;
83
            }
84
        } else {
85 4
            if ($type) {
86 2
                throw new ContainerException(
87 2
                    "Autowiring does not support union or intersection types. Source: \n" .
88 2
                        $this->getParamInfo($param)
89 2
                );
90
            }
91
92 2
            throw new ContainerException(
93 2
                "Autowired entities need to have typed constructor parameters. Source: \n" .
94 2
                    $this->getParamInfo($param)
95 2
            );
96
        }
97
    }
98
99 6
    public function getParamInfo(ReflectionParameter $param): string
100
    {
101 6
        $type = $param->getType();
102 6
        $rf = $param->getDeclaringFunction();
103 6
        $rc = null;
104
105 6
        if ($rf instanceof ReflectionMethod) {
106 6
            $rc = $rf->getDeclaringClass();
107
        }
108
109 6
        return ($rc ? $rc->getName() . '::' : '') .
110 6
            ($rf->getName() . '(..., ') .
111 6
            ($type ? (string)$type . ' ' : '') .
112 6
            '$' . $param->getName() . ', ...)';
113
    }
114
115
    /** @psalm-param callable-array|callable $callable */
116 23
    public function resolveCallableArgs(array|callable $callable, array $predefinedArgs = []): array
117
    {
118 23
        $callable = Closure::fromCallable($callable);
119 23
        $rf = new ReflectionFunction($callable);
120 23
        $predefinedArgs = array_merge($this->getInjectedArgs($rf), $predefinedArgs);
121
122 23
        return $this->resolveArgs($rf, $predefinedArgs);
123
    }
124
125
    /** @psalm-param ReflectionClass|class-string $class */
126 102
    public function resolveConstructorArgs(ReflectionClass|string $class, array $predefinedArgs = []): array
127
    {
128 102
        $rc = is_string($class) ? new ReflectionClass($class) : $class;
129 102
        $constructor = $rc->getConstructor();
130
131 102
        if ($constructor) {
132 93
            $combinedArgs = array_merge($this->getInjectedArgs($constructor), $predefinedArgs);
133
134 93
            return $this->resolveArgs($constructor, $combinedArgs);
135
        }
136
137 19
        return $predefinedArgs;
138
    }
139
140 100
    protected function resolveArgs(
141
        ?ReflectionFunctionAbstract $rf,
142
        array $predefinedArgs = [],
143
    ): array {
144 100
        $args = [];
145
146 100
        if ($rf) {
147 100
            foreach ($rf->getParameters() as $param) {
148 56
                $name = $param->getName();
149
150 56
                if (isset($predefinedArgs[$name])) {
151
                    /** @psalm-var list<mixed> */
152 29
                    $args[] = $predefinedArgs[$name];
153
                } else {
154
                    /** @psalm-var list<mixed> */
155 53
                    $args[] = $this->resolveParam($param);
156
                }
157
            }
158
        }
159
160 96
        return $args;
161
    }
162
163 100
    protected function getInjectedArgs(ReflectionFunctionAbstract $rf): array
164
    {
165
        /** @psalm-var array<non-empty-string, mixed> */
166 100
        $result = [];
167 100
        $injectAttrs = $rf->getAttributes(Inject::class);
168
169 100
        foreach ($injectAttrs as $injectAttr) {
170 3
            $instance = $injectAttr->newInstance();
171
172
            /** @psalm-suppress MixedAssignment */
173 3
            foreach ($instance->args as $name => $value) {
174 3
                assert(is_string($name));
175
176 3
                if (is_string($value)) {
177 3
                    if ($this->registry->has($value)) {
178 3
                        $result[$name] = $this->registry->get($value);
179 3
                    } elseif (class_exists($value)) {
180 2
                        $result[$name] = $this->autowire($value);
181
                    } else {
182 3
                        $result[$name] = $value;
183
                    }
184
                } else {
185 2
                    $result[$name] = $value;
186
                }
187
            }
188
        }
189
190 100
        return $result;
191
    }
192
}
193