Resolver   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 194
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 33
eloc 94
dl 0
loc 194
ccs 99
cts 99
cp 1
rs 9.76
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A resolveCallableArgs() 0 7 1
A resolveConstructorArgs() 0 12 3
A getParamInfo() 0 14 4
A autowire() 0 35 5
A resolveParam() 0 25 5
A resolveCallAttributes() 0 17 2
A resolveArgs() 0 29 6
A getInjectedArgs() 0 28 6
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 160
    public function __construct(protected readonly Registry $registry)
23
    {
24 160
    }
25
26
    /** @psalm-param class-string $class */
27 106
    public function autowire(
28
        string $class,
29
        array $predefinedArgs = [],
30
        ?string $constructor = null
31
    ): object {
32 106
        if (!$this->registry->autowire) {
33
            try {
34 2
                $this->registry->new($class, ...$predefinedArgs);
35 1
            } catch (Throwable $e) {
36 1
                throw new ContainerException(
37 1
                    "Autowiring is turned off. Tried to instantiate class '{$class}'"
38 1
                );
39
            }
40
        }
41
42 105
        $rc = new ReflectionClass($class);
43
44
        try {
45 105
            if ($constructor) {
46
                // Factory method
47 12
                $rm = $rc->getMethod($constructor);
48 12
                $args = $this->resolveArgs($rm, $predefinedArgs);
49 12
                $instance = $rm->invoke(null, ...$args);
50
            } else {
51
                // Regular constructor
52 103
                $args = $this->resolveConstructorArgs($rc, $predefinedArgs);
53 100
                $instance = $rc->newInstance(...$args);
54
            }
55
56 101
            assert(is_object($instance));
57
58 101
            return $this->resolveCallAttributes($instance);
59 6
        } catch (Throwable $e) {
60 6
            throw new ContainerException(
61 6
                'Autowiring unresolvable: ' . $class . ' Details: ' . $e->getMessage()
62 6
            );
63
        }
64
    }
65
66 109
    public function resolveCallAttributes(object $instance): object
67
    {
68 109
        $callAttrs = (new ReflectionObject($instance))->getAttributes(Call::class);
69
70
        // See if the attribute itself has one or more Call attributes. If so,
71
        // resolve/autowire the arguments of the method it states and call it.
72 109
        foreach ($callAttrs as $callAttr) {
73 3
            $callAttr = $callAttr->newInstance();
74 3
            $methodToResolve = $callAttr->method;
75
76
            /** @psalm-var callable */
77 3
            $callable = [$instance, $methodToResolve];
78 3
            $args = $this->resolveCallableArgs($callable, $callAttr->args);
79 3
            $callable(...$args);
80
        }
81
82 109
        return $instance;
83
    }
84
85 65
    public function resolveParam(ReflectionParameter $param): mixed
86
    {
87 65
        $type = $param->getType();
88
89 65
        if ($type instanceof ReflectionNamedType) {
90
            try {
91 61
                return $this->registry->get(ltrim($type->getName(), '?'));
92 9
            } catch (NotFoundException | ContainerException  $e) {
93 9
                if ($param->isDefaultValueAvailable()) {
94 7
                    return $param->getDefaultValue();
95
                }
96
97 2
                throw $e;
98
            }
99
        } else {
100 4
            if ($type) {
101 2
                throw new ContainerException(
102 2
                    "Autowiring does not support union or intersection types. Source: \n" .
103 2
                        $this->getParamInfo($param)
104 2
                );
105
            }
106
107 2
            throw new ContainerException(
108 2
                "Autowired entities need to have typed constructor parameters. Source: \n" .
109 2
                    $this->getParamInfo($param)
110 2
            );
111
        }
112
    }
113
114 6
    public function getParamInfo(ReflectionParameter $param): string
115
    {
116 6
        $type = $param->getType();
117 6
        $rf = $param->getDeclaringFunction();
118 6
        $rc = null;
119
120 6
        if ($rf instanceof ReflectionMethod) {
121 6
            $rc = $rf->getDeclaringClass();
122
        }
123
124 6
        return ($rc ? $rc->getName() . '::' : '') .
125 6
            ($rf->getName() . '(..., ') .
126 6
            ($type ? (string)$type . ' ' : '') .
127 6
            '$' . $param->getName() . ', ...)';
128
    }
129
130
    /** @psalm-param callable-array|callable $callable */
131 14
    public function resolveCallableArgs(array|callable $callable, array $predefinedArgs = []): array
132
    {
133 14
        $callable = Closure::fromCallable($callable);
134 14
        $rf = new ReflectionFunction($callable);
135 14
        $predefinedArgs = array_merge($this->getInjectedArgs($rf), $predefinedArgs);
136
137 14
        return $this->resolveArgs($rf, $predefinedArgs);
138
    }
139
140
    /** @psalm-param ReflectionClass|class-string $class */
141 103
    public function resolveConstructorArgs(ReflectionClass|string $class, array $predefinedArgs = []): array
142
    {
143 103
        $rc = is_string($class) ? new ReflectionClass($class) : $class;
144 103
        $constructor = $rc->getConstructor();
145
146 103
        if ($constructor) {
147 92
            $combinedArgs = array_merge($this->getInjectedArgs($constructor), $predefinedArgs);
148
149 92
            return $this->resolveArgs($constructor, $combinedArgs);
150
        }
151
152 21
        return $predefinedArgs;
153
    }
154
155 103
    protected function resolveArgs(
156
        ?ReflectionFunctionAbstract $rf,
157
        array $predefinedArgs = [],
158
    ): array {
159 103
        $args = [];
160
161 103
        if ($rf) {
162 103
            $parameters = $rf->getParameters();
163 103
            $countPredefined = count($predefinedArgs);
164
165 103
            if (array_is_list($predefinedArgs) && $countPredefined > 0) {
0 ignored issues
show
Bug introduced by
The function array_is_list was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

165
            if (/** @scrutinizer ignore-call */ array_is_list($predefinedArgs) && $countPredefined > 0) {
Loading history...
166
                // predefined args are not named, use them as they are
167 4
                $args = $predefinedArgs;
168 4
                $parameters = array_slice($parameters, $countPredefined);
169
            }
170
171 103
            foreach ($parameters as $param) {
172 55
                $name = $param->getName();
173 55
                if (isset($predefinedArgs[$name])) {
174
                    /** @psalm-var list<mixed> */
175 26
                    $args[] = $predefinedArgs[$name];
176
                } else {
177
                    /** @psalm-var list<mixed> */
178 52
                    $args[] = $this->resolveParam($param);
179
                }
180
            }
181
        }
182
183 99
        return $args;
184
    }
185
186 99
    protected function getInjectedArgs(ReflectionFunctionAbstract $rf): array
187
    {
188
        /** @psalm-var array<non-empty-string, mixed> */
189 99
        $result = [];
190 99
        $injectAttrs = $rf->getAttributes(Inject::class);
191
192 99
        foreach ($injectAttrs as $injectAttr) {
193 3
            $instance = $injectAttr->newInstance();
194
195
            /** @psalm-suppress MixedAssignment */
196 3
            foreach ($instance->args as $name => $value) {
197 3
                assert(is_string($name));
198
199 3
                if (is_string($value)) {
200 3
                    if ($this->registry->has($value)) {
201 3
                        $result[$name] = $this->registry->get($value);
202 3
                    } elseif (class_exists($value)) {
203 2
                        $result[$name] = $this->autowire($value);
204
                    } else {
205 3
                        $result[$name] = $value;
206
                    }
207
                } else {
208 2
                    $result[$name] = $value;
209
                }
210
            }
211
        }
212
213 99
        return $result;
214
    }
215
}
216