1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | /** |
||
6 | * Micro\Container |
||
7 | * |
||
8 | * @copyright Copryright (c) 2018-2019 gyselroth GmbH (https://gyselroth.com) |
||
9 | * @license MIT https://opensource.org/licenses/MIT |
||
10 | */ |
||
11 | |||
12 | namespace Micro\Container; |
||
13 | |||
14 | use ProxyManager\Factory\LazyLoadingValueHolderFactory; |
||
15 | use Psr\Container\ContainerInterface; |
||
16 | use ReflectionClass; |
||
17 | use ReflectionMethod; |
||
18 | use ReflectionParameter; |
||
19 | use RuntimeException; |
||
20 | |||
21 | class RuntimeContainer |
||
22 | { |
||
23 | /** |
||
24 | * Config. |
||
25 | * |
||
26 | * @var Config |
||
27 | */ |
||
28 | protected $config; |
||
29 | |||
30 | /** |
||
31 | * Service registry. |
||
32 | * |
||
33 | * @var array |
||
34 | */ |
||
35 | protected $service = []; |
||
36 | |||
37 | /** |
||
38 | * Parent container. |
||
39 | * |
||
40 | * @var ContainerInterface|RuntimeContainer |
||
41 | */ |
||
42 | protected $parent; |
||
43 | |||
44 | /** |
||
45 | * Children container. |
||
46 | * |
||
47 | * @var ContainerInterface[] |
||
48 | */ |
||
49 | protected $children = []; |
||
50 | |||
51 | /** |
||
52 | * Parent service. |
||
53 | * |
||
54 | * @var mixed |
||
55 | */ |
||
56 | protected $parent_service; |
||
57 | |||
58 | /** |
||
59 | * Create container. |
||
60 | */ |
||
61 | 64 | public function __construct(iterable $config, $parent, ContainerInterface $interface) |
|
62 | { |
||
63 | 64 | $this->config = new Config($config, $this); |
|
64 | 64 | $this->parent = $parent; |
|
65 | 64 | $this->service[ContainerInterface::class] = $interface; |
|
66 | 64 | } |
|
67 | |||
68 | /** |
||
69 | * Get parent container. |
||
70 | */ |
||
71 | 58 | public function getParent() |
|
72 | { |
||
73 | 58 | return $this->parent; |
|
74 | } |
||
75 | |||
76 | /** |
||
77 | * Set parent service on container. |
||
78 | */ |
||
79 | 2 | public function setParentService($service) |
|
80 | { |
||
81 | 2 | $this->parent_service = $service; |
|
82 | |||
83 | 2 | return $this; |
|
84 | } |
||
85 | |||
86 | /** |
||
87 | * Get config. |
||
88 | */ |
||
89 | 2 | public function getConfig(): Config |
|
90 | { |
||
91 | 2 | return $this->config; |
|
92 | } |
||
93 | |||
94 | /** |
||
95 | * Get service. |
||
96 | */ |
||
97 | 64 | public function get(string $name, ?array $parameters = null) |
|
98 | { |
||
99 | try { |
||
100 | 64 | return $this->resolve($name, $parameters); |
|
101 | 20 | } catch (Exception\ServiceNotFound $e) { |
|
102 | 16 | return $this->wrapService($name, $parameters); |
|
103 | } |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * Resolve service. |
||
108 | */ |
||
109 | 64 | public function resolve(string $name, ?array $parameters = null) |
|
110 | { |
||
111 | 64 | if (isset($this->service[$name])) { |
|
112 | 7 | return $this->service[$name]; |
|
113 | } |
||
114 | |||
115 | 62 | if ($this->config->has($name)) { |
|
116 | 54 | return $this->wrapService($name, $parameters); |
|
117 | } |
||
118 | |||
119 | 17 | if (null !== $this->parent_service) { |
|
120 | 1 | $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service)); |
|
121 | |||
122 | 1 | if (in_array($name, $parents, true) && $this->parent_service instanceof $name) { |
|
123 | return $this->parent_service; |
||
124 | } |
||
125 | } |
||
126 | |||
127 | 17 | if (null !== $this->parent) { |
|
128 | 1 | return $this->parent->resolve($name, $parameters); |
|
129 | } |
||
130 | |||
131 | 16 | throw new Exception\ServiceNotFound("service $name was not found in service tree"); |
|
132 | } |
||
133 | |||
134 | /** |
||
135 | * Store service. |
||
136 | */ |
||
137 | 52 | protected function storeService(string $name, array $config, $service) |
|
138 | { |
||
139 | 52 | if (false === $config['singleton']) { |
|
140 | 6 | return $service; |
|
141 | } |
||
142 | 46 | $this->service[$name] = $service; |
|
143 | |||
144 | 46 | if (isset($this->children[$name])) { |
|
145 | 2 | $this->children[$name]->setParentService($service); |
|
146 | } |
||
147 | |||
148 | 46 | return $service; |
|
149 | } |
||
150 | |||
151 | /** |
||
152 | * Wrap resolved service in callable if enabled. |
||
153 | */ |
||
154 | 62 | protected function wrapService(string $name, ?array $parameters = null) |
|
155 | { |
||
156 | 62 | $config = $this->config->get($name); |
|
157 | 59 | if (true === $config['wrap']) { |
|
158 | 1 | $that = $this; |
|
159 | |||
160 | 1 | return function () use ($that, $name, $parameters) { |
|
161 | 1 | return $that->autoWireClass($name, $parameters); |
|
162 | 1 | }; |
|
163 | } |
||
164 | |||
165 | 58 | return $this->autoWireClass($name, $parameters); |
|
166 | } |
||
167 | |||
168 | /** |
||
169 | * Auto wire. |
||
170 | */ |
||
171 | 59 | protected function autoWireClass(string $name, ?array $parameters = null) |
|
172 | { |
||
173 | 59 | $config = $this->config->get($name); |
|
174 | 59 | $class = $config['use']; |
|
175 | |||
176 | 59 | if (null !== $parameters) { |
|
177 | 3 | $config['singleton'] = false; |
|
178 | } |
||
179 | |||
180 | 59 | if (preg_match('#^\{([^{}]+)\}$#', $class, $match)) { |
|
181 | 1 | return $this->wireReference($name, $match[1], $config); |
|
182 | } |
||
183 | |||
184 | 59 | $reflection = new ReflectionClass($class); |
|
185 | |||
186 | 59 | if (isset($config['factory'])) { |
|
187 | 3 | $factory = $reflection->getMethod($config['factory']); |
|
188 | 3 | $args = $this->autoWireMethod($name, $factory, $config, $parameters); |
|
189 | 3 | $instance = call_user_func_array([$class, $config['factory']], $args); |
|
190 | |||
191 | 3 | return $this->prepareService($name, $instance, $reflection, $config); |
|
192 | } |
||
193 | |||
194 | 56 | $constructor = $reflection->getConstructor(); |
|
195 | |||
196 | 56 | if (null === $constructor) { |
|
197 | 8 | return $this->createInstance($name, $reflection, [], $config); |
|
198 | } |
||
199 | |||
200 | 48 | $args = $this->autoWireMethod($name, $constructor, $config, $parameters); |
|
201 | |||
202 | 42 | return $this->createInstance($name, $reflection, $args, $config); |
|
203 | } |
||
204 | |||
205 | /** |
||
206 | * Wire named referenced service. |
||
207 | */ |
||
208 | 1 | protected function wireReference(string $name, string $reference, array $config) |
|
209 | { |
||
210 | 1 | $service = $this->get($reference); |
|
211 | 1 | $reflection = new ReflectionClass(get_class($service)); |
|
212 | 1 | $config = $this->config->get($name); |
|
213 | 1 | $service = $this->prepareService($name, $service, $reflection, $config); |
|
214 | |||
215 | 1 | return $service; |
|
216 | } |
||
217 | |||
218 | /** |
||
219 | * Get instance (virtual or real instance). |
||
220 | */ |
||
221 | 50 | protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config) |
|
222 | { |
||
223 | 50 | if (true === $config['lazy']) { |
|
224 | 2 | return $this->getProxyInstance($name, $class, $arguments, $config); |
|
225 | } |
||
226 | |||
227 | 48 | return $this->getRealInstance($name, $class, $arguments, $config); |
|
228 | } |
||
229 | |||
230 | /** |
||
231 | * Create proxy instance. |
||
232 | */ |
||
233 | 2 | protected function getProxyInstance(string $name, ReflectionClass $class, array $arguments, array $config) |
|
234 | { |
||
235 | 2 | $factory = new LazyLoadingValueHolderFactory(); |
|
236 | 2 | $that = $this; |
|
237 | |||
238 | 2 | return $factory->createProxy( |
|
239 | 2 | $class->getName(), |
|
240 | 2 | function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) use ($that, $name,$class,$arguments,$config) { |
|
241 | 1 | $wrappedObject = $that->getRealInstance($name, $class, $arguments, $config); |
|
242 | 1 | $initializer = null; |
|
243 | 2 | } |
|
244 | ); |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * Create real instance. |
||
249 | */ |
||
250 | 49 | protected function getRealInstance(string $name, ReflectionClass $class, array $arguments, array $config) |
|
251 | { |
||
252 | 49 | $instance = $class->newInstanceArgs($arguments); |
|
253 | 49 | $instance = $this->prepareService($name, $instance, $class, $config); |
|
254 | |||
255 | 49 | return $instance; |
|
256 | } |
||
257 | |||
258 | /** |
||
259 | * Prepare service (execute sub selects and excute setter injections). |
||
260 | */ |
||
261 | 52 | protected function prepareService(string $name, $service, ReflectionClass $class, array $config) |
|
262 | { |
||
263 | 52 | $this->storeService($name, $config, $service); |
|
264 | |||
265 | 52 | foreach ($config['calls'] as $call) { |
|
266 | 15 | if (!is_array($call)) { |
|
267 | 1 | continue; |
|
268 | } |
||
269 | |||
270 | 15 | if (!isset($call['method'])) { |
|
271 | throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name); |
||
272 | } |
||
273 | |||
274 | 15 | $arguments = []; |
|
275 | 15 | $result = null; |
|
276 | |||
277 | try { |
||
278 | 15 | $method = $class->getMethod($call['method']); |
|
279 | } catch (\ReflectionException $e) { |
||
280 | throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name); |
||
281 | } |
||
282 | |||
283 | 15 | if (isset($call['batch']) && is_array($call['batch'])) { |
|
284 | 1 | foreach ($call['batch'] as $sub) { |
|
285 | 1 | $args = array_combine($call['arguments'], $sub); |
|
286 | 1 | $arguments = $this->autoWireMethod($name, $method, $call, $args); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
287 | 1 | $result = call_user_func_array([&$service, $call['method']], $arguments); |
|
288 | } |
||
289 | } else { |
||
290 | 14 | $arguments = $this->autoWireMethod($name, $method, $call); |
|
291 | 14 | $result = call_user_func_array([&$service, $call['method']], $arguments); |
|
292 | } |
||
293 | |||
294 | 15 | if (isset($call['select']) && true === $call['select']) { |
|
295 | 2 | $service = $result; |
|
296 | } |
||
297 | } |
||
298 | |||
299 | 52 | $this->storeService($name, $config, $service); |
|
300 | |||
301 | 52 | return $service; |
|
302 | } |
||
303 | |||
304 | /** |
||
305 | * Autowire method. |
||
306 | */ |
||
307 | 56 | protected function autoWireMethod(string $name, ReflectionMethod $method, array $config, ?array $parameters = null): array |
|
308 | { |
||
309 | 56 | $params = $method->getParameters(); |
|
310 | 56 | $args = []; |
|
311 | |||
312 | 56 | foreach ($params as $param) { |
|
313 | 56 | $type = $param->getClass(); |
|
314 | 56 | $param_name = $param->getName(); |
|
315 | |||
316 | 56 | $hint = $param->getType(); |
|
317 | 56 | if (null === $hint) { |
|
318 | 8 | $hint = 'string'; |
|
319 | } else { |
||
320 | 49 | $hint = $hint->getName(); |
|
321 | } |
||
322 | |||
323 | 56 | if (isset($parameters[$param_name])) { |
|
324 | 4 | $args[$param_name] = $parameters[$param_name]; |
|
325 | 55 | } elseif (isset($config['arguments'][$param_name])) { |
|
326 | 44 | $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name, $hint); |
|
327 | 23 | } elseif (null !== $type) { |
|
328 | 10 | $args[$param_name] = $this->resolveServiceArgument($name, $type, $param); |
|
329 | 15 | } elseif ($param->isDefaultValueAvailable()) { |
|
330 | 12 | $args[$param_name] = $param->getDefaultValue(); |
|
331 | 4 | } elseif ($param->allowsNull()) { |
|
332 | 1 | $args[$param_name] = null; |
|
333 | } else { |
||
334 | 3 | throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name); |
|
335 | } |
||
336 | |||
337 | 50 | if (!$param->canBePassedByValue()) { |
|
338 | 2 | $value = &$args[$param_name]; |
|
339 | 2 | $args[$param_name] = &$value; |
|
340 | } |
||
341 | } |
||
342 | |||
343 | 50 | return $args; |
|
344 | } |
||
345 | |||
346 | /** |
||
347 | * Resolve service argument. |
||
348 | */ |
||
349 | 10 | protected function resolveServiceArgument(string $name, ReflectionClass $type, ReflectionParameter $param) |
|
350 | { |
||
351 | 10 | $type_class = $type->getName(); |
|
352 | |||
353 | 10 | if ($type_class === $name) { |
|
354 | 1 | throw new RuntimeException('class '.$type_class.' can not depend on itself'); |
|
355 | } |
||
356 | |||
357 | try { |
||
358 | 9 | return $this->traverseTree($name, $type_class); |
|
359 | 2 | } catch (\Exception $e) { |
|
360 | 2 | if ($param->isDefaultValueAvailable() && null === $param->getDefaultValue()) { |
|
361 | 1 | return null; |
|
362 | } |
||
363 | |||
364 | 1 | throw $e; |
|
365 | } |
||
366 | } |
||
367 | |||
368 | /** |
||
369 | * Parse param value. |
||
370 | */ |
||
371 | 44 | protected function parseParam($param, string $name, string $type = 'string') |
|
372 | { |
||
373 | 44 | if (is_iterable($param)) { |
|
374 | 1 | foreach ($param as $key => $value) { |
|
375 | 1 | $param[$key] = $this->parseParam($value, $name, 'string'); |
|
376 | } |
||
377 | |||
378 | 1 | return $param; |
|
379 | } |
||
380 | |||
381 | 44 | if (is_string($param)) { |
|
382 | 42 | $param = $this->config->getEnv($param, $type); |
|
383 | 41 | if (is_string($param)) { |
|
384 | 37 | if (preg_match('#^\{\{([^{}]+)\}\}$#', $param, $matches)) { |
|
385 | 1 | return '{'.$matches[1].'}'; |
|
386 | } |
||
387 | 36 | if (preg_match('#^\{([^{}]+)\}$#', $param, $matches)) { |
|
388 | 1 | return $this->traverseTree($name, $matches[1]); |
|
389 | } |
||
390 | } |
||
391 | |||
392 | 40 | return $param; |
|
393 | } |
||
394 | 2 | if ($param instanceof \Closure) { |
|
395 | 1 | $param = $param->bindTo($this); |
|
396 | } |
||
397 | |||
398 | 2 | return $param; |
|
399 | } |
||
400 | |||
401 | /** |
||
402 | * Locate service. |
||
403 | */ |
||
404 | 10 | protected function traverseTree(string $current_service, string $service) |
|
405 | { |
||
406 | 10 | if (isset($this->children[$current_service])) { |
|
407 | 1 | return $this->children[$current_service]->get($service); |
|
408 | } |
||
409 | |||
410 | 10 | $config = $this->config->get($current_service); |
|
411 | 10 | if (isset($config['services'])) { |
|
412 | 2 | $this->children[$current_service] = new self($config['services'], $this, $this->service[ContainerInterface::class]); |
|
413 | |||
414 | 2 | return $this->children[$current_service]->get($service); |
|
415 | } |
||
416 | |||
417 | 9 | return $this->get($service); |
|
418 | } |
||
419 | } |
||
420 |