1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Gravatalonga\Container; |
||
6 | |||
7 | use ArrayAccess; |
||
8 | use Closure; |
||
9 | use Psr\Container\ContainerInterface; |
||
10 | use ReflectionClass; |
||
11 | use ReflectionException; |
||
12 | use ReflectionFunction; |
||
13 | use ReflectionParameter; |
||
14 | use Reflector; |
||
15 | |||
16 | use function array_key_exists; |
||
17 | use function is_callable; |
||
18 | |||
19 | /** |
||
20 | * Class Container. |
||
21 | */ |
||
22 | class Container extends AutoWiringAware implements ArrayAccess, ContainerInterface |
||
23 | { |
||
24 | /** |
||
25 | * @var ContainerInterface |
||
26 | */ |
||
27 | protected static $instance; |
||
28 | |||
29 | /** |
||
30 | * @var array<string, string> |
||
31 | */ |
||
32 | private array $aliases = []; |
||
33 | |||
34 | /** |
||
35 | * @var array<string, mixed> |
||
36 | */ |
||
37 | private array $bindings; |
||
38 | |||
39 | /** |
||
40 | * @var array<string, boolean> |
||
41 | */ |
||
42 | private array $entriesBeingResolved = []; |
||
43 | |||
44 | /** |
||
45 | * @var array <string, mixed> |
||
46 | */ |
||
47 | private array $extended = []; |
||
48 | |||
49 | /** |
||
50 | * @var array<string, mixed> |
||
51 | */ |
||
52 | private array $resolved = []; |
||
53 | |||
54 | /** |
||
55 | * @var array<string, mixed> |
||
56 | */ |
||
57 | private array $share; |
||
58 | |||
59 | /** |
||
60 | * Container constructor. |
||
61 | * |
||
62 | * @param array<string, mixed> $config |
||
63 | * |
||
64 | * @throws NotFoundContainerException |
||
65 | */ |
||
66 | 171 | public function __construct(array $config = []) |
|
67 | { |
||
68 | 171 | $this->bindings = $config; |
|
69 | 171 | $this->share = []; |
|
70 | |||
71 | 171 | $self = $this; |
|
72 | 171 | $this->share(ContainerInterface::class, static function () use ($self) { |
|
73 | 33 | return $self; |
|
74 | }); |
||
75 | 171 | $this->alias(ContainerInterface::class, Container::class); |
|
76 | } |
||
77 | |||
78 | /** |
||
79 | * @param string $entry |
||
80 | * @param string $alias |
||
81 | * |
||
82 | * @throws NotFoundContainerException |
||
83 | * |
||
84 | * @return void |
||
85 | */ |
||
86 | 171 | public function alias($entry, $alias) |
|
87 | { |
||
88 | 171 | if ($this->isAlias($entry)) { |
|
89 | 3 | throw NotFoundContainerException::entryNotFound($entry); |
|
90 | } |
||
91 | |||
92 | 171 | if (!$this->has($entry)) { |
|
93 | 3 | throw NotFoundContainerException::entryNotFound($entry); |
|
94 | } |
||
95 | |||
96 | 171 | $this->aliases[$alias] = $entry; |
|
97 | } |
||
98 | |||
99 | /** |
||
100 | * @param string $id |
||
101 | * @param callable|mixed $factory |
||
102 | * |
||
103 | * @throws NotFoundContainerException |
||
104 | * |
||
105 | * @return void |
||
106 | */ |
||
107 | 24 | public function extend($id, $factory) |
|
108 | { |
||
109 | 24 | if (!$this->has($id)) { |
|
110 | 3 | throw NotFoundContainerException::entryNotFound($id); |
|
111 | } |
||
112 | |||
113 | 21 | $factory = $this->prepareEntry($factory); |
|
114 | |||
115 | 21 | if (array_key_exists($id, $this->resolved)) { |
|
116 | 3 | unset($this->resolved[$id]); |
|
117 | } |
||
118 | |||
119 | 21 | $this->extended[$id][] = $factory; |
|
120 | } |
||
121 | |||
122 | /** |
||
123 | * Factory binding. |
||
124 | * |
||
125 | * @param callable|mixed $factory |
||
126 | * |
||
127 | * @return void |
||
128 | */ |
||
129 | 90 | public function factory(string $id, $factory) |
|
130 | { |
||
131 | 90 | $this->bindings[$id] = $this->prepareEntry($factory); |
|
132 | } |
||
133 | |||
134 | /** |
||
135 | * {@inheritdoc} |
||
136 | * |
||
137 | * @throws ReflectionException |
||
138 | */ |
||
139 | 129 | public function get(string $id) |
|
140 | { |
||
141 | 129 | return $this->resolve($id, []); |
|
142 | } |
||
143 | |||
144 | /** |
||
145 | * @return ContainerInterface |
||
146 | */ |
||
147 | 3 | public static function getInstance() |
|
148 | { |
||
149 | 3 | return self::$instance; |
|
150 | } |
||
151 | |||
152 | /** |
||
153 | * {@inheritdoc} |
||
154 | */ |
||
155 | 171 | public function has(string $id): bool |
|
156 | { |
||
157 | 171 | return array_key_exists($id, $this->bindings) |
|
158 | 171 | || array_key_exists($id, $this->share) |
|
159 | 171 | || array_key_exists($id, $this->aliases); |
|
160 | } |
||
161 | |||
162 | /** |
||
163 | * @param string $id |
||
164 | * |
||
165 | * @return bool |
||
166 | */ |
||
167 | 171 | public function isAlias($id) |
|
168 | { |
||
169 | 171 | return array_key_exists($id, $this->aliases); |
|
170 | } |
||
171 | |||
172 | /** |
||
173 | * @param string $id |
||
174 | * @param array<string, mixed> $arguments |
||
175 | * |
||
176 | * @throws NotFoundContainerException |
||
177 | * @throws ContainerException|ReflectionException |
||
178 | * |
||
179 | * @return mixed|object |
||
180 | */ |
||
181 | 24 | public function make($id, array $arguments = []) |
|
182 | { |
||
183 | 24 | if (array_key_exists($id, $this->share)) { |
|
184 | 3 | throw ContainerException::shareOnMake($id); |
|
185 | } |
||
186 | |||
187 | 21 | return $this->resolve($id, $arguments); |
|
188 | } |
||
189 | |||
190 | /** |
||
191 | * @param string $offset |
||
192 | * |
||
193 | * @return bool |
||
194 | */ |
||
195 | 6 | #[\ReturnTypeWillChange] |
|
196 | public function offsetExists($offset) |
||
197 | { |
||
198 | 6 | return $this->has($offset); |
|
199 | } |
||
200 | |||
201 | /** |
||
202 | * @param string $offset |
||
203 | * |
||
204 | * @throws ReflectionException |
||
205 | * |
||
206 | * @return mixed |
||
207 | */ |
||
208 | 6 | #[\ReturnTypeWillChange] |
|
209 | public function offsetGet($offset) |
||
210 | { |
||
211 | 6 | return $this->get($offset); |
|
212 | } |
||
213 | |||
214 | /** |
||
215 | * @param string $offset |
||
216 | * @param mixed $value |
||
217 | */ |
||
218 | 9 | #[\ReturnTypeWillChange] |
|
219 | public function offsetSet($offset, $value): void |
||
220 | { |
||
221 | 9 | $this->factory($offset, $value); |
|
222 | } |
||
223 | |||
224 | /** |
||
225 | * @param string $offset |
||
226 | * |
||
227 | * @return void |
||
228 | */ |
||
229 | 6 | #[\ReturnTypeWillChange] |
|
230 | public function offsetUnset($offset) |
||
231 | { |
||
232 | unset( |
||
233 | 6 | $this->bindings[$offset], |
|
234 | 6 | $this->share[$offset], |
|
235 | 6 | $this->resolved[$offset], |
|
236 | 6 | $this->aliases[$offset], |
|
237 | 6 | $this->extended[$offset] |
|
238 | ); |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Alias for Factory method. |
||
243 | * |
||
244 | * @param mixed $factory |
||
245 | * |
||
246 | * @return void |
||
247 | */ |
||
248 | 45 | public function set(string $id, $factory) |
|
249 | { |
||
250 | 45 | $this->factory($id, $factory); |
|
251 | } |
||
252 | |||
253 | 3 | public static function setInstance(ContainerInterface $container): void |
|
254 | { |
||
255 | 3 | self::$instance = $container; |
|
256 | } |
||
257 | |||
258 | /** |
||
259 | * Share rather resolve as factory. |
||
260 | * |
||
261 | * @param string $id |
||
262 | * @param mixed $factory |
||
263 | * |
||
264 | * @return void |
||
265 | */ |
||
266 | 171 | public function share($id, $factory) |
|
267 | { |
||
268 | 171 | if (array_key_exists($id, $this->resolved)) { |
|
269 | 3 | unset($this->resolved[$id]); |
|
270 | } |
||
271 | |||
272 | 171 | $this->share[$id] = $this->prepareEntry($factory); |
|
273 | } |
||
274 | |||
275 | /** |
||
276 | * @param ReflectionParameter[] $params |
||
277 | * @param array<string, mixed> $arguments |
||
278 | * |
||
279 | * @return array<int, string> |
||
280 | */ |
||
281 | 123 | private function buildDependencies(array $params, array $arguments = []) |
|
282 | { |
||
283 | 123 | return array_map( |
|
284 | 123 | function (ReflectionParameter $param) use ($arguments) { |
|
285 | 69 | if (array_key_exists($param->getName(), $this->entriesBeingResolved)) { |
|
286 | 3 | throw ContainerException::circularDependency(); |
|
287 | } |
||
288 | |||
289 | 69 | $this->entriesBeingResolved[$param->getName()] = true; |
|
290 | |||
291 | 69 | if (array_key_exists($param->getName(), $arguments)) { |
|
292 | 9 | return $arguments[$param->getName()]; |
|
293 | } |
||
294 | |||
295 | 66 | $type = $param->getType(); |
|
296 | |||
297 | // https://github.com/phpstan/phpstan/issues/1133 |
||
298 | // @phpstan-ignore-next-line |
||
299 | 66 | if (null !== $type && array_key_exists($type->getName(), $arguments)) { |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
300 | // @phpstan-ignore-next-line |
||
301 | 3 | return $arguments[$type->getName()]; |
|
302 | } |
||
303 | |||
304 | 63 | return $this->autoWiringArguments($param); |
|
305 | }, |
||
306 | $params |
||
307 | ); |
||
308 | } |
||
309 | |||
310 | /** |
||
311 | * Get all extenders for particular entry id. |
||
312 | * |
||
313 | * @return array|mixed |
||
314 | */ |
||
315 | 129 | private function getExtenders(string $id) |
|
316 | { |
||
317 | 129 | return $this->extended[$id] ?? []; |
|
318 | } |
||
319 | |||
320 | /** |
||
321 | * @param mixed $factory |
||
322 | * |
||
323 | * @return callable|Closure |
||
324 | */ |
||
325 | 171 | private function prepareEntry($factory) |
|
326 | { |
||
327 | 171 | return is_callable($factory) ? |
|
328 | 171 | ($factory instanceof Closure ? |
|
329 | 171 | $factory : |
|
330 | 171 | Closure::fromCallable($factory)) : |
|
331 | 171 | $factory; |
|
332 | } |
||
333 | |||
334 | /** |
||
335 | * @param array<string, mixed> $arguments |
||
336 | * |
||
337 | * @throws NotFoundContainerException |
||
338 | * @throws ReflectionException |
||
339 | * |
||
340 | * @return mixed|object |
||
341 | */ |
||
342 | 138 | private function resolve(string $id, array $arguments = []) |
|
343 | { |
||
344 | 138 | if (array_key_exists($id, $this->resolved)) { |
|
345 | 21 | return $this->resolved[$id]; |
|
346 | } |
||
347 | |||
348 | 138 | if (array_key_exists($id, $this->aliases)) { |
|
349 | 12 | return $this->resolve($this->aliases[$id], $arguments); |
|
350 | } |
||
351 | |||
352 | 138 | if ((!$this->has($id)) && (class_exists($id))) { |
|
353 | 48 | return $this->resolveClass($id, $arguments); |
|
354 | } |
||
355 | 39 | ||
356 | if ($this->has($id)) { |
||
357 | $get = $this->resolveEntry($id, $arguments); |
||
358 | |||
359 | foreach ($this->getExtenders($id) as $extend) { |
||
360 | 39 | if (is_callable($extend)) { |
|
361 | $get = $extend($this, $get); |
||
362 | $this->resolved[$id] = $get; |
||
363 | 111 | continue; |
|
364 | 108 | } |
|
365 | $get = $extend; |
||
366 | 108 | $this->resolved[$id] = $get; |
|
367 | 18 | } |
|
368 | 15 | ||
369 | 15 | return $get; |
|
370 | 15 | } |
|
371 | |||
372 | 3 | throw NotFoundContainerException::entryNotFound($id); |
|
373 | 3 | } |
|
374 | |||
375 | /** |
||
376 | 108 | * @param array<string, mixed> $arguments |
|
377 | * |
||
378 | * @return array<int, mixed> |
||
379 | 3 | */ |
|
380 | private function resolveArguments(Reflector $reflection, array $arguments = []) |
||
381 | { |
||
382 | $params = []; |
||
383 | |||
384 | if ($reflection instanceof ReflectionClass && null !== $constructor = $reflection->getConstructor()) { |
||
385 | $params = $constructor->getParameters(); |
||
386 | } |
||
387 | 123 | ||
388 | if ($reflection instanceof ReflectionFunction) { |
||
389 | 123 | $params = $reflection->getParameters(); |
|
390 | } |
||
391 | 123 | ||
392 | 45 | $value = $this->buildDependencies($params, $arguments); |
|
393 | $this->entriesBeingResolved = []; |
||
394 | |||
395 | 123 | return $value; |
|
396 | 84 | } |
|
397 | |||
398 | /** |
||
399 | 123 | * @param mixed $id |
|
400 | 117 | * @param array<string, mixed> $arguments |
|
401 | * |
||
402 | 117 | * @throws ReflectionException |
|
403 | * |
||
404 | * @return object |
||
405 | */ |
||
406 | private function resolveClass($id, array $arguments = []) |
||
407 | { |
||
408 | $reflection = new ReflectionClass($id); |
||
409 | |||
410 | return $reflection->newInstanceArgs($this->resolveArguments($reflection, $arguments)); |
||
411 | } |
||
412 | |||
413 | 48 | /** |
|
414 | * @param array<string, mixed> $arguments |
||
415 | 48 | * |
|
416 | * @throws ReflectionException |
||
417 | 48 | * |
|
418 | * @return mixed |
||
419 | */ |
||
420 | private function resolveEntry(string $id, array $arguments = []) |
||
421 | { |
||
422 | $get = $this->bindings[$id] ?? $this->share[$id]; |
||
423 | |||
424 | if ($get instanceof Closure) { |
||
425 | $reflection = new ReflectionFunction($get); |
||
426 | $value = $reflection->invokeArgs($this->resolveArguments($reflection, $arguments)); |
||
427 | 108 | ||
428 | if (array_key_exists($id, $this->share)) { |
||
429 | 108 | $this->resolved[$id] = $value; |
|
430 | } |
||
431 | 108 | ||
432 | 84 | return $value; |
|
433 | 84 | } |
|
434 | |||
435 | 84 | return $get; |
|
436 | } |
||
437 | } |