1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Balloon |
7
|
|
|
* |
8
|
|
|
* @author Raffael Sahli <[email protected]> |
9
|
|
|
* @copyright Copryright (c) 2012-2017 gyselroth GmbH (https://gyselroth.com) |
10
|
|
|
* @license GPL-3.0 https://opensource.org/licenses/GPL-3.0 |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace Micro\Container; |
14
|
|
|
|
15
|
|
|
use Closure; |
16
|
|
|
use Psr\Container\ContainerInterface; |
17
|
|
|
use ReflectionClass; |
18
|
|
|
use ReflectionMethod; |
19
|
|
|
use Micro\Container\Exception; |
20
|
|
|
|
21
|
|
|
class Container implements ContainerInterface |
22
|
|
|
{ |
23
|
|
|
/** |
24
|
|
|
* Config. |
25
|
|
|
* |
26
|
|
|
* @var array |
27
|
|
|
*/ |
28
|
|
|
protected $config = []; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Service registry. |
32
|
|
|
* |
33
|
|
|
* @var array |
34
|
|
|
*/ |
35
|
|
|
protected $service = []; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Registered but not initialized service registry. |
39
|
|
|
* |
40
|
|
|
* @var array |
41
|
|
|
*/ |
42
|
|
|
protected $registry = []; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Create container. |
46
|
|
|
* |
47
|
|
|
* @param Iterable $config |
48
|
|
|
*/ |
49
|
|
|
public function __construct(Iterable $config = []) |
50
|
|
|
{ |
51
|
|
|
$this->config = $config; |
|
|
|
|
52
|
|
|
$this->add(ContainerInterface::class, $this); |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Get service. |
57
|
|
|
* |
58
|
|
|
* @param string $name |
59
|
|
|
* |
60
|
|
|
* @return mixed |
61
|
|
|
*/ |
62
|
|
|
public function get($name) |
63
|
|
|
{ |
64
|
|
|
if ($this->has($name)) { |
65
|
|
|
return $this->service[$name]['instance']; |
66
|
|
|
} |
67
|
|
|
if (isset($this->registry[$name])) { |
68
|
|
|
if( $this->registry[$name] instanceof Closure) { |
69
|
|
|
$this->service[$name]['instance'] = $this->registry[$name]->call($this); |
70
|
|
|
} else { |
71
|
|
|
$this->service[$name]['instance'] = $this->registry[$name]; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
unset($this->registry[$name]); |
75
|
|
|
return $this->service[$name]['instance']; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
return $this->autoWireClass($name); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Debug container service tree |
84
|
|
|
* |
85
|
|
|
* @return array |
86
|
|
|
*/ |
87
|
|
|
public function __debug(?array $container=null): array |
88
|
|
|
{ |
89
|
|
|
if($container === null) { |
90
|
|
|
$container = $this->service; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
foreach($container as $name => &$service) { |
94
|
|
|
if(isset($service['instance'])) { |
95
|
|
|
$service['instance'] = 'instanceof '.get_class($service['instance']); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
if(isset($service['services'])) { |
99
|
|
|
$service['services'] = $this->__debug($service['services']); |
100
|
|
|
} |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
return $container; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Get new instance (Do not store in container). |
109
|
|
|
* |
110
|
|
|
* @param string $name |
111
|
|
|
* |
112
|
|
|
* @return mixed |
113
|
|
|
*/ |
114
|
|
|
public function getNew(string $name) |
115
|
|
|
{ |
116
|
|
|
if (isset($this->registry[$name])) { |
117
|
|
|
return $this->registry[$name]->call($this); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
return $this->autoWireClass($name); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Add service. |
125
|
|
|
* |
126
|
|
|
* @param string $name |
127
|
|
|
* @param mixed $service |
128
|
|
|
* |
129
|
|
|
* @return Container |
130
|
|
|
*/ |
131
|
|
|
public function add(string $name, $service): self |
132
|
|
|
{ |
133
|
|
|
if ($this->has($name)) { |
134
|
|
|
throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered'); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$this->registry[$name] = $service; |
138
|
|
|
|
139
|
|
|
return $this; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Check if service is registered. |
144
|
|
|
* |
145
|
|
|
* @param mixed $name |
146
|
|
|
* |
147
|
|
|
* @return bool |
148
|
|
|
*/ |
149
|
|
|
public function has($name): bool |
150
|
|
|
{ |
151
|
|
|
return isset($this->service[$name]); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Auto wire. |
156
|
|
|
* |
157
|
|
|
* @param string $name |
158
|
|
|
* @param array $config |
159
|
|
|
* @param array $parents |
160
|
|
|
* |
161
|
|
|
* @return mixed |
162
|
|
|
*/ |
163
|
|
|
protected function autoWireClass(string $name, ?array $config = null, array $parents = []) |
164
|
|
|
{ |
165
|
|
|
if (null === $config) { |
166
|
|
|
$config = $this->config; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$class = $name; |
170
|
|
|
$sub_config = $config; |
|
|
|
|
171
|
|
|
if (isset($config[$name])) { |
172
|
|
|
if (isset($config[$name]['use'])) { |
173
|
|
|
$class = $config[$name]['use']; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
$config = $config[$name]; |
177
|
|
|
} else { |
178
|
|
|
$config = []; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
try { |
182
|
|
|
$reflection = new ReflectionClass($class); |
183
|
|
|
} catch (\Exception $e) { |
184
|
|
|
throw new Exception\Configuration($class.' can not be resolved to an existing class for service '.$name); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$constructor = $reflection->getConstructor(); |
188
|
|
|
|
189
|
|
|
if (null === $constructor) { |
190
|
|
|
return new $class(); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
$args = $this->autoWireMethod($name, $constructor, $config, $parents); |
194
|
|
|
return $this->createInstance($name, $reflection, $args, $config, $parents); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Traverse services with parents and find correct service to use. |
199
|
|
|
* |
200
|
|
|
* @param string $name |
201
|
|
|
* @param string $class |
202
|
|
|
* @param mixed $config |
203
|
|
|
* @param mixed $parents |
204
|
|
|
* |
205
|
|
|
* @return mixed |
206
|
|
|
*/ |
207
|
|
|
protected function findParentService(string $name, ?string $class, $config, $parents) |
208
|
|
|
{ |
209
|
|
|
$service = null; |
|
|
|
|
210
|
|
|
$services = $this->service; |
211
|
|
|
|
212
|
|
|
foreach (array_reverse($parents) as $name => $parent) { |
213
|
|
|
if (isset($services[$name])) { |
214
|
|
|
$service = $services[$name]; |
215
|
|
|
if (isset($services['services'])) { |
216
|
|
|
$services = $services['services']; |
217
|
|
|
} else { |
218
|
|
|
break; |
219
|
|
|
} |
220
|
|
|
} else { |
221
|
|
|
break; |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
foreach (array_reverse($parents) as $parent) { |
226
|
|
|
if (isset($parent['services'][$class])) { |
227
|
|
|
return $this->autoWireClass($class, $parent['services'], $parents); |
|
|
|
|
228
|
|
|
} |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
return $this->get($class); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Create instance. |
236
|
|
|
* |
237
|
|
|
* @param string $name |
238
|
|
|
* @param ReflectionClass $class |
239
|
|
|
* @param array $args |
240
|
|
|
* @param mixed $parents |
241
|
|
|
* |
242
|
|
|
* @return mixed |
243
|
|
|
*/ |
244
|
|
|
protected function createInstance(string $name, ReflectionClass $class, array $args, array $config, $parents = []) |
245
|
|
|
{ |
246
|
|
|
$instance = $class->newInstanceArgs($args); |
247
|
|
|
|
248
|
|
|
$loop = &$this->service; |
249
|
|
|
foreach ($parents as $p => $parent) { |
250
|
|
|
$loop = &$loop[$p]; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
if (0 === count($parents)) { |
254
|
|
|
$loop[$name]['instance'] = $instance; |
255
|
|
|
} else { |
256
|
|
|
$loop['services'][$name]['instance'] = $instance; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
$parents[$name] = $config; |
260
|
|
|
$parents_orig = $parents; |
|
|
|
|
261
|
|
|
|
262
|
|
|
if(isset($config['calls'])) { |
263
|
|
|
foreach($config['calls'] as $call) { |
264
|
|
|
$arguments = []; |
|
|
|
|
265
|
|
|
try { |
266
|
|
|
$method = $class->getMethod($call['method']); |
267
|
|
|
} catch(\ReflectionException $e) { |
268
|
|
|
throw new Exception\Configuration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
$arguments = $this->autoWireMethod($name, $method, $call, $parents); |
272
|
|
|
call_user_func_array([&$instance, $call['method']], $arguments); |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
return $instance; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* Autowire method |
281
|
|
|
* |
282
|
|
|
* @param string $name |
283
|
|
|
* @param ReflectionMethod $method |
284
|
|
|
* @param array $config |
285
|
|
|
* @param mixed $parents |
286
|
|
|
* @return array |
287
|
|
|
*/ |
288
|
|
|
protected function autoWireMethod(string $name, ReflectionMethod $method, array $config, $parents): array |
289
|
|
|
{ |
290
|
|
|
$params = $method->getParameters(); |
291
|
|
|
$args = []; |
292
|
|
|
|
293
|
|
|
foreach ($params as $param) { |
294
|
|
|
$type = $param->getClass(); |
295
|
|
|
$param_name = $param->getName(); |
296
|
|
|
|
297
|
|
|
if(isset($config['arguments'][$param_name])) { |
298
|
|
|
$args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name, $type, $config, $parents); |
299
|
|
|
} elseif($type !== null) { |
300
|
|
|
$type_class = $type->getName(); |
301
|
|
|
|
302
|
|
|
if ($type_class === $name) { |
303
|
|
|
throw new Exception\Logic('class '.$type_class.' can not depend on itself'); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
$args[$param_name] = $this->findParentService($name, $type_class, $config, $parents); |
307
|
|
|
} elseif($param->isDefaultValueAvailable()) { |
308
|
|
|
$args[$param_name] = $param->getDefaultValue(); |
309
|
|
|
} elseif($param->allowsNull() && $param->hasType()) { |
310
|
|
|
$args[$param_name] = null; |
311
|
|
|
} else { |
312
|
|
|
throw new Exception\Configuration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name); |
313
|
|
|
} |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
return $args; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Parse param value. |
322
|
|
|
* |
323
|
|
|
* @param mixed $param |
324
|
|
|
* @param string $name |
325
|
|
|
* @param string $type_class |
326
|
|
|
* @param array $config |
327
|
|
|
* @param mixed $parents |
328
|
|
|
* |
329
|
|
|
* @return mixed |
330
|
|
|
*/ |
331
|
|
|
protected function parseParam($param, string $name, $type_class, array $config, $parents) |
332
|
|
|
{ |
333
|
|
|
if (is_iterable($param)) { |
334
|
|
|
foreach ($param as $key => $value) { |
335
|
|
|
$param[$key] = $this->parseParam($value, $name, $type_class, $config, $parents); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
return $param; |
339
|
|
|
} |
340
|
|
|
if (is_string($param)) { |
341
|
|
|
if (preg_match('#\{ENV\(([A-Za-z0-9_]+)(?:(,?)(.*))\)\}#', $param, $match)) { |
342
|
|
|
if (4 !== count($match)) { |
343
|
|
|
return $param; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
$env = getenv($match[1]); |
347
|
|
|
if (false === $env && !empty($match[3])) { |
348
|
|
|
return str_replace($match[0], $match[3], $param); |
349
|
|
|
} |
350
|
|
|
if (false === $env) { |
351
|
|
|
throw new Exception\EnvVariableNotFound('env variable '.$match[1].' required but it is neither set not a default value exists'); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
return str_replace($match[0], $env, $param); |
355
|
|
|
} elseif(preg_match('#^\{(.*)\}$#', $param, $match)) { |
356
|
|
|
return $this->findParentService($match[1], $match[1], $config, $parents); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
return $param; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
return $param; |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
|
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
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. 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.