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
![]() |
|||
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 |