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); |
|||||
0 ignored issues
–
show
|
|||||||
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); |
|||||
0 ignored issues
–
show
The method
setParentService() does not exist on Psr\Container\ContainerInterface .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
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
It seems like
$args can also be of type false ; however, parameter $parameters of Micro\Container\RuntimeContainer::autoWireMethod() does only seem to accept array|null , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
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 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.