|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
declare(strict_types=1); |
|
4
|
|
|
|
|
5
|
|
|
/** |
|
6
|
|
|
* Micro\Container |
|
7
|
|
|
* |
|
8
|
|
|
* @copyright Copryright (c) 2018 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 |
|
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
|
|
|
* @param iterable $config |
|
62
|
|
|
* @param ContainerInterface|RuntimeContainer $parent |
|
63
|
|
|
*/ |
|
64
|
47 |
|
public function __construct(Iterable $config = [], $parent = null) |
|
65
|
|
|
{ |
|
66
|
47 |
|
$this->config = new Config($config, $this); |
|
67
|
47 |
|
$this->parent = $parent; |
|
|
|
|
|
|
68
|
47 |
|
$this->service[ContainerInterface::class] = $this; |
|
69
|
47 |
|
} |
|
70
|
|
|
|
|
71
|
|
|
/** |
|
72
|
|
|
* Get parent container. |
|
73
|
|
|
* |
|
74
|
|
|
* @return ContainerInterface|RuntimeContainer |
|
75
|
|
|
*/ |
|
76
|
42 |
|
public function getParent() |
|
77
|
|
|
{ |
|
78
|
42 |
|
return $this->parent; |
|
79
|
|
|
} |
|
80
|
|
|
|
|
81
|
|
|
/** |
|
82
|
|
|
* Set parent service on container. |
|
83
|
|
|
* |
|
84
|
|
|
* @param mixed $service |
|
85
|
|
|
* |
|
86
|
|
|
* @return ContainerInterface|RuntimeContainer |
|
87
|
|
|
*/ |
|
88
|
2 |
|
public function setParentService($service) |
|
89
|
|
|
{ |
|
90
|
2 |
|
$this->parent_service = $service; |
|
91
|
|
|
|
|
92
|
2 |
|
return $this; |
|
93
|
|
|
} |
|
94
|
|
|
|
|
95
|
|
|
/** |
|
96
|
|
|
* Get config. |
|
97
|
|
|
* |
|
98
|
|
|
* @return Config |
|
99
|
|
|
*/ |
|
100
|
2 |
|
public function getConfig(): Config |
|
101
|
|
|
{ |
|
102
|
2 |
|
return $this->config; |
|
103
|
|
|
} |
|
104
|
|
|
|
|
105
|
|
|
/** |
|
106
|
|
|
* Get service. |
|
107
|
|
|
* |
|
108
|
|
|
* @param string $name |
|
109
|
|
|
* |
|
110
|
|
|
* @return mixed |
|
111
|
|
|
*/ |
|
112
|
47 |
|
public function get(string $name) |
|
113
|
|
|
{ |
|
114
|
|
|
try { |
|
115
|
47 |
|
return $this->resolve($name); |
|
116
|
18 |
|
} catch (Exception\ServiceNotFound $e) { |
|
117
|
13 |
|
return $this->wrapService($name); |
|
118
|
|
|
} |
|
119
|
|
|
} |
|
120
|
|
|
|
|
121
|
|
|
/** |
|
122
|
|
|
* Resolve service. |
|
123
|
|
|
* |
|
124
|
|
|
* @param string $name |
|
125
|
|
|
* |
|
126
|
|
|
* @return mixed |
|
127
|
|
|
*/ |
|
128
|
47 |
|
public function resolve(string $name) |
|
129
|
|
|
{ |
|
130
|
47 |
|
if (isset($this->service[$name])) { |
|
131
|
6 |
|
return $this->service[$name]; |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
46 |
|
if ($this->config->has($name)) { |
|
135
|
41 |
|
return $this->wrapService($name); |
|
136
|
|
|
} |
|
137
|
|
|
|
|
138
|
14 |
|
if (null !== $this->parent_service) { |
|
139
|
1 |
|
$parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service)); |
|
140
|
|
|
|
|
141
|
1 |
|
if (in_array($name, $parents, true) && $this->parent_service instanceof $name) { |
|
142
|
|
|
return $this->parent_service; |
|
143
|
|
|
} |
|
144
|
|
|
} |
|
145
|
|
|
|
|
146
|
14 |
|
if (null !== $this->parent) { |
|
147
|
1 |
|
return $this->parent->resolve($name); |
|
|
|
|
|
|
148
|
|
|
} |
|
149
|
|
|
|
|
150
|
13 |
|
throw new Exception\ServiceNotFound("service $name was not found in service tree"); |
|
151
|
|
|
} |
|
152
|
|
|
|
|
153
|
|
|
/** |
|
154
|
|
|
* Store service. |
|
155
|
|
|
* |
|
156
|
|
|
* @param param string $name |
|
157
|
|
|
* @param array $config |
|
158
|
|
|
* @param mixed $service |
|
159
|
|
|
* |
|
160
|
|
|
* @return mixed |
|
161
|
|
|
*/ |
|
162
|
38 |
|
protected function storeService(string $name, array $config, $service) |
|
163
|
|
|
{ |
|
164
|
38 |
|
if (true === $config['singleton']) { |
|
165
|
2 |
|
return $service; |
|
166
|
|
|
} |
|
167
|
37 |
|
$this->service[$name] = $service; |
|
168
|
|
|
|
|
169
|
37 |
|
if (isset($this->children[$name])) { |
|
170
|
2 |
|
$this->children[$name]->setParentService($service); |
|
|
|
|
|
|
171
|
|
|
} |
|
172
|
|
|
|
|
173
|
37 |
|
return $service; |
|
174
|
|
|
} |
|
175
|
|
|
|
|
176
|
|
|
/** |
|
177
|
|
|
* Wrap resolved service in callable if enabled. |
|
178
|
|
|
* |
|
179
|
|
|
* @param string $name |
|
180
|
|
|
* |
|
181
|
|
|
* @return mixed |
|
182
|
|
|
*/ |
|
183
|
46 |
|
protected function wrapService(string $name) |
|
184
|
|
|
{ |
|
185
|
46 |
|
$config = $this->config->get($name); |
|
186
|
43 |
|
if (true === $config['wrap']) { |
|
187
|
1 |
|
$that = $this; |
|
188
|
|
|
|
|
189
|
1 |
|
return function () use ($that, $name) { |
|
190
|
1 |
|
return $that->autoWireClass($name); |
|
191
|
1 |
|
}; |
|
192
|
|
|
} |
|
193
|
|
|
|
|
194
|
42 |
|
return $this->autoWireClass($name); |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
/** |
|
198
|
|
|
* Auto wire. |
|
199
|
|
|
* |
|
200
|
|
|
* @param string $name |
|
201
|
|
|
* |
|
202
|
|
|
* @return mixed |
|
203
|
|
|
*/ |
|
204
|
43 |
|
protected function autoWireClass(string $name) |
|
205
|
|
|
{ |
|
206
|
43 |
|
$config = $this->config->get($name); |
|
207
|
43 |
|
$class = $config['use']; |
|
208
|
|
|
|
|
209
|
43 |
|
if (preg_match('#^\{([^{}]+)\}$#', $class, $match)) { |
|
210
|
1 |
|
return $this->wireReference($name, $match[1], $config); |
|
211
|
|
|
} |
|
212
|
|
|
|
|
213
|
43 |
|
if (isset($config['factory'])) { |
|
214
|
4 |
|
if (!isset($config['factory']['method'])) { |
|
215
|
1 |
|
throw new Exception\InvalidConfiguration('method is required for factory in service '.$name); |
|
216
|
|
|
} |
|
217
|
|
|
|
|
218
|
3 |
|
if (isset($config['factory']['use'])) { |
|
219
|
1 |
|
$class = $config['factory']['use']; |
|
220
|
|
|
} |
|
221
|
|
|
|
|
222
|
3 |
|
$reflection = new ReflectionClass($class); |
|
223
|
3 |
|
$factory = $reflection->getMethod($config['factory']['method']); |
|
224
|
3 |
|
$args = $this->autoWireMethod($name, $factory, $config['factory']); |
|
225
|
3 |
|
$instance = call_user_func_array([$class, $config['factory']['method']], $args); |
|
226
|
|
|
|
|
227
|
3 |
|
return $this->prepareService($name, $instance, $reflection, $config); |
|
228
|
|
|
} |
|
229
|
|
|
|
|
230
|
39 |
|
$reflection = new ReflectionClass($class); |
|
231
|
39 |
|
$constructor = $reflection->getConstructor(); |
|
232
|
|
|
|
|
233
|
39 |
|
if (null === $constructor) { |
|
234
|
3 |
|
return $this->storeService($name, $config, new $class()); |
|
235
|
|
|
} |
|
236
|
|
|
|
|
237
|
36 |
|
$args = $this->autoWireMethod($name, $constructor, $config); |
|
238
|
|
|
|
|
239
|
33 |
|
return $this->createInstance($name, $reflection, $args, $config); |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
/** |
|
243
|
|
|
* Wire named referenced service. |
|
244
|
|
|
* |
|
245
|
|
|
* @param string $name |
|
246
|
|
|
* @param string $refrence |
|
247
|
|
|
* @param array $config |
|
248
|
|
|
* |
|
249
|
|
|
* @return mixed |
|
250
|
|
|
*/ |
|
251
|
1 |
|
protected function wireReference(string $name, string $reference, array $config) |
|
252
|
|
|
{ |
|
253
|
1 |
|
$service = $this->get($reference); |
|
254
|
1 |
|
$reflection = new ReflectionClass(get_class($service)); |
|
255
|
1 |
|
$config = $this->config->get($name); |
|
256
|
1 |
|
$service = $this->prepareService($name, $service, $reflection, $config); |
|
257
|
|
|
|
|
258
|
1 |
|
return $service; |
|
259
|
|
|
} |
|
260
|
|
|
|
|
261
|
|
|
/** |
|
262
|
|
|
* Get instance (virtual or real instance). |
|
263
|
|
|
* |
|
264
|
|
|
* @param string $name |
|
265
|
|
|
* @param ReflectionClass $class |
|
266
|
|
|
* @param array $arguments |
|
267
|
|
|
* @param array $config |
|
268
|
|
|
* |
|
269
|
|
|
* @return mixed |
|
270
|
|
|
*/ |
|
271
|
33 |
|
protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config) |
|
272
|
|
|
{ |
|
273
|
33 |
|
if (true === $config['lazy']) { |
|
274
|
2 |
|
return $this->getProxyInstance($name, $class, $arguments, $config); |
|
275
|
|
|
} |
|
276
|
|
|
|
|
277
|
31 |
|
return $this->getRealInstance($name, $class, $arguments, $config); |
|
278
|
|
|
} |
|
279
|
|
|
|
|
280
|
|
|
/** |
|
281
|
|
|
* Create proxy instance. |
|
282
|
|
|
* |
|
283
|
|
|
* @param string $name |
|
284
|
|
|
* @param ReflectionClass $class |
|
285
|
|
|
* @param array $arguments |
|
286
|
|
|
* @param array $config |
|
287
|
|
|
* |
|
288
|
|
|
* @return mixed |
|
289
|
|
|
*/ |
|
290
|
2 |
|
protected function getProxyInstance(string $name, ReflectionClass $class, array $arguments, array $config) |
|
291
|
|
|
{ |
|
292
|
2 |
|
$factory = new LazyLoadingValueHolderFactory(); |
|
293
|
2 |
|
$that = $this; |
|
294
|
|
|
|
|
295
|
2 |
|
return $factory->createProxy( |
|
296
|
2 |
|
$class->getName(), |
|
297
|
2 |
|
function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) use ($that, $name,$class,$arguments,$config) { |
|
298
|
1 |
|
$wrappedObject = $that->getRealInstance($name, $class, $arguments, $config); |
|
299
|
1 |
|
$initializer = null; |
|
300
|
2 |
|
} |
|
301
|
|
|
); |
|
302
|
|
|
} |
|
303
|
|
|
|
|
304
|
|
|
/** |
|
305
|
|
|
* Create real instance. |
|
306
|
|
|
* |
|
307
|
|
|
* @param string $name |
|
308
|
|
|
* @param ReflectionClass $class |
|
309
|
|
|
* @param array $arguments |
|
310
|
|
|
* @param array $config |
|
311
|
|
|
* |
|
312
|
|
|
* @return mixed |
|
313
|
|
|
*/ |
|
314
|
32 |
|
protected function getRealInstance(string $name, ReflectionClass $class, array $arguments, array $config) |
|
315
|
|
|
{ |
|
316
|
32 |
|
$instance = $class->newInstanceArgs($arguments); |
|
317
|
32 |
|
$instance = $this->prepareService($name, $instance, $class, $config); |
|
318
|
|
|
|
|
319
|
30 |
|
return $instance; |
|
320
|
|
|
} |
|
321
|
|
|
|
|
322
|
|
|
/** |
|
323
|
|
|
* Prepare service (execute sub selects and excute setter injections). |
|
324
|
|
|
* |
|
325
|
|
|
* @param string $name |
|
326
|
|
|
* @param mixed $service |
|
327
|
|
|
* @param ReflectionClass $class |
|
328
|
|
|
* @param array $config |
|
329
|
|
|
* |
|
330
|
|
|
* @return mixed |
|
331
|
|
|
*/ |
|
332
|
35 |
|
protected function prepareService(string $name, $service, ReflectionClass $class, array $config) |
|
333
|
|
|
{ |
|
334
|
35 |
|
foreach ($config['selects'] as $select) { |
|
335
|
2 |
|
$args = $this->autoWireMethod($name, $class->getMethod($select['method']), $select); |
|
336
|
2 |
|
$service = call_user_func_array([&$service, $select['method']], $args); |
|
337
|
|
|
} |
|
338
|
|
|
|
|
339
|
35 |
|
$this->storeService($name, $config, $service); |
|
340
|
|
|
|
|
341
|
35 |
|
foreach ($config['calls'] as $call) { |
|
342
|
8 |
|
if (null === $call) { |
|
343
|
|
|
continue; |
|
344
|
|
|
} |
|
345
|
|
|
|
|
346
|
8 |
|
if (!isset($call['method'])) { |
|
347
|
1 |
|
throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name); |
|
348
|
|
|
} |
|
349
|
|
|
|
|
350
|
7 |
|
$arguments = []; |
|
|
|
|
|
|
351
|
|
|
|
|
352
|
|
|
try { |
|
353
|
7 |
|
$method = $class->getMethod($call['method']); |
|
354
|
1 |
|
} catch (\ReflectionException $e) { |
|
355
|
1 |
|
throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name); |
|
356
|
|
|
} |
|
357
|
|
|
|
|
358
|
6 |
|
$arguments = $this->autoWireMethod($name, $method, $call); |
|
359
|
6 |
|
call_user_func_array([&$service, $call['method']], $arguments); |
|
360
|
|
|
} |
|
361
|
|
|
|
|
362
|
33 |
|
return $service; |
|
363
|
|
|
} |
|
364
|
|
|
|
|
365
|
|
|
/** |
|
366
|
|
|
* Autowire method. |
|
367
|
|
|
* |
|
368
|
|
|
* @param string $name |
|
369
|
|
|
* @param ReflectionMethod $method |
|
370
|
|
|
* @param array $config |
|
371
|
|
|
* |
|
372
|
|
|
* @return array |
|
373
|
|
|
*/ |
|
374
|
39 |
|
protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array |
|
375
|
|
|
{ |
|
376
|
39 |
|
$params = $method->getParameters(); |
|
377
|
39 |
|
$args = []; |
|
378
|
|
|
|
|
379
|
39 |
|
foreach ($params as $param) { |
|
380
|
39 |
|
$type = $param->getClass(); |
|
381
|
39 |
|
$param_name = $param->getName(); |
|
382
|
|
|
|
|
383
|
39 |
|
if (isset($config['arguments'][$param_name])) { |
|
384
|
33 |
|
$args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name); |
|
385
|
18 |
|
} elseif (null !== $type) { |
|
386
|
10 |
|
$args[$param_name] = $this->resolveServiceArgument($name, $type, $param); |
|
387
|
10 |
|
} elseif ($param->isDefaultValueAvailable()) { |
|
388
|
10 |
|
$args[$param_name] = $param->getDefaultValue(); |
|
389
|
1 |
|
} elseif ($param->allowsNull()) { |
|
390
|
1 |
|
$args[$param_name] = null; |
|
391
|
|
|
} else { |
|
392
|
36 |
|
throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name); |
|
393
|
|
|
} |
|
394
|
|
|
} |
|
395
|
|
|
|
|
396
|
36 |
|
return $args; |
|
397
|
|
|
} |
|
398
|
|
|
|
|
399
|
|
|
/** |
|
400
|
|
|
* Resolve service argument. |
|
401
|
|
|
* |
|
402
|
|
|
* @param string $name |
|
403
|
|
|
* @param ReflectionClass $type |
|
404
|
|
|
* @param ReflectionParameter $param |
|
405
|
|
|
* |
|
406
|
|
|
* @return mixed |
|
407
|
|
|
*/ |
|
408
|
10 |
|
protected function resolveServiceArgument(string $name, ReflectionClass $type, ReflectionParameter $param) |
|
409
|
|
|
{ |
|
410
|
10 |
|
$type_class = $type->getName(); |
|
411
|
|
|
|
|
412
|
10 |
|
if ($type_class === $name) { |
|
413
|
1 |
|
throw new RuntimeException('class '.$type_class.' can not depend on itself'); |
|
414
|
|
|
} |
|
415
|
|
|
|
|
416
|
|
|
try { |
|
417
|
9 |
|
return $this->traverseTree($name, $type_class); |
|
418
|
2 |
|
} catch (\Exception $e) { |
|
419
|
2 |
|
if ($param->isDefaultValueAvailable() && null === $param->getDefaultValue()) { |
|
420
|
1 |
|
return null; |
|
421
|
|
|
} |
|
422
|
|
|
|
|
423
|
1 |
|
throw $e; |
|
424
|
|
|
} |
|
425
|
|
|
} |
|
426
|
|
|
|
|
427
|
|
|
/** |
|
428
|
|
|
* Parse param value. |
|
429
|
|
|
* |
|
430
|
|
|
* @param mixed $param |
|
431
|
|
|
* @param string $name |
|
432
|
|
|
* |
|
433
|
|
|
* @return mixed |
|
434
|
|
|
*/ |
|
435
|
33 |
|
protected function parseParam($param, string $name) |
|
436
|
|
|
{ |
|
437
|
33 |
|
if (is_iterable($param)) { |
|
438
|
1 |
|
foreach ($param as $key => $value) { |
|
439
|
1 |
|
$param[$key] = $this->parseParam($value, $name); |
|
440
|
|
|
} |
|
441
|
|
|
|
|
442
|
1 |
|
return $param; |
|
443
|
|
|
} |
|
444
|
|
|
|
|
445
|
33 |
|
if (is_string($param)) { |
|
446
|
32 |
|
$param = $this->config->getEnv($param); |
|
447
|
|
|
|
|
448
|
31 |
|
if (preg_match('#^\{\{([^{}]+)\}\}$#', $param, $matches)) { |
|
449
|
1 |
|
return '{'.$matches[1].'}'; |
|
450
|
|
|
} |
|
451
|
30 |
|
if (preg_match('#^\{([^{}]+)\}$#', $param, $matches)) { |
|
452
|
1 |
|
return $this->traverseTree($name, $matches[1]); |
|
453
|
|
|
} |
|
454
|
|
|
|
|
455
|
30 |
|
return $param; |
|
456
|
|
|
} |
|
457
|
|
|
|
|
458
|
1 |
|
return $param; |
|
459
|
|
|
} |
|
460
|
|
|
|
|
461
|
|
|
/** |
|
462
|
|
|
* Locate service. |
|
463
|
|
|
* |
|
464
|
|
|
* @param string $current_service |
|
465
|
|
|
* @param string $service |
|
466
|
|
|
*/ |
|
467
|
10 |
|
protected function traverseTree(string $current_service, string $service) |
|
468
|
|
|
{ |
|
469
|
10 |
|
if (isset($this->children[$current_service])) { |
|
470
|
1 |
|
return $this->children[$current_service]->get($service); |
|
471
|
|
|
} |
|
472
|
|
|
|
|
473
|
10 |
|
$config = $this->config->get($current_service); |
|
474
|
10 |
|
if (isset($config['services'])) { |
|
475
|
2 |
|
$this->children[$current_service] = new self($config['services'], $this); |
|
476
|
|
|
|
|
477
|
2 |
|
return $this->children[$current_service]->get($service); |
|
478
|
|
|
} |
|
479
|
|
|
|
|
480
|
9 |
|
return $this->get($service); |
|
481
|
|
|
} |
|
482
|
|
|
} |
|
483
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.