1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* slince dependency injection component |
4
|
|
|
* @author Tao <[email protected]> |
5
|
|
|
*/ |
6
|
|
|
namespace Slince\Di; |
7
|
|
|
|
8
|
|
|
use Psr\Container\ContainerInterface; |
9
|
|
|
use Slince\Di\Exception\ConfigException; |
10
|
|
|
use Slince\Di\Exception\DependencyInjectionException; |
11
|
|
|
use Slince\Di\Exception\NotFoundException; |
12
|
|
|
|
13
|
|
|
class Container implements ContainerInterface |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* Array of singletons |
17
|
|
|
* @var array |
18
|
|
|
*/ |
19
|
|
|
protected $shares = []; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Array pf definitions, support instance,callable,Definition, class |
23
|
|
|
* @var array |
24
|
|
|
*/ |
25
|
|
|
protected $definitions = []; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Array of interface bindings |
29
|
|
|
* @var array |
30
|
|
|
*/ |
31
|
|
|
protected $contextBindings = []; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Array of parameters |
35
|
|
|
* @var ParameterStore |
36
|
|
|
*/ |
37
|
|
|
protected $parameters; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var ClassDefinitionResolver |
41
|
|
|
*/ |
42
|
|
|
protected $classDefinitionResolver; |
43
|
|
|
|
44
|
|
|
public function __construct() |
45
|
|
|
{ |
46
|
|
|
$this->parameters = new ParameterStore(); |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Add a Definition class |
51
|
|
|
* @param string $name |
52
|
|
|
* @param string $class |
53
|
|
|
* @return ClassDefinition |
54
|
|
|
*/ |
55
|
|
|
public function define($name, $class) |
56
|
|
|
{ |
57
|
|
|
$definition = new ClassDefinition($class); |
58
|
|
|
$this->definitions[$name] = $definition; |
59
|
|
|
return $definition; |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Bind an callable to the container with its name |
64
|
|
|
* @param string $name |
65
|
|
|
* @param mixed $creation A invalid callable |
66
|
|
|
* @throws ConfigException |
67
|
|
|
* @return $this |
68
|
|
|
*/ |
69
|
|
|
public function call($name, $creation) |
70
|
|
|
{ |
71
|
|
|
if (!is_callable($creation)) { |
72
|
|
|
throw new ConfigException(sprintf("Delegate expects a valid callable or executable class::method string at Argument 2")); |
73
|
|
|
} |
74
|
|
|
$this->definitions[$name] = $creation; |
75
|
|
|
return $this; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Bind an instance to the container with its name |
80
|
|
|
* ``` |
81
|
|
|
* $container->instance('user', new User()); |
82
|
|
|
* //Or just give instance |
83
|
|
|
* $container->instance(new User()); |
84
|
|
|
* |
85
|
|
|
* ``` |
86
|
|
|
* @param string $name |
87
|
|
|
* @param object $instance |
88
|
|
|
* @throws ConfigException |
89
|
|
|
* @return $this |
90
|
|
|
*/ |
91
|
|
|
public function instance($name, $instance = null) |
92
|
|
|
{ |
93
|
|
|
if (func_num_args() == 1) { |
94
|
|
|
if (!is_object($name)) { |
95
|
|
|
throw new ConfigException(sprintf("Instance expects a valid object")); |
96
|
|
|
} |
97
|
|
|
$instance = $name; |
98
|
|
|
$name = get_class($instance); |
99
|
|
|
} |
100
|
|
|
$this->definitions[$name] = $instance; |
101
|
|
|
$this->share($name); |
102
|
|
|
return $this; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Binds an interface or abstract class to its implementation; |
107
|
|
|
* It's also be used to bind a service name to an existing class |
108
|
|
|
* @param string $name |
109
|
|
|
* @param string $implementation |
110
|
|
|
* @param string|array $context the specified context to bind |
111
|
|
|
* @throws ConfigException |
112
|
|
|
* @return $this |
113
|
|
|
*/ |
114
|
|
|
public function bind($name, $implementation, $context = null) |
115
|
|
|
{ |
116
|
|
|
if (is_null($context)) { |
117
|
|
|
$this->define($name, $implementation); |
118
|
|
|
} else { |
119
|
|
|
if (is_array($context)) { |
120
|
|
|
list($contextClass, $contextMethod) = $context; |
121
|
|
|
} else { |
122
|
|
|
$contextClass = $context; |
123
|
|
|
$contextMethod = 'general'; |
124
|
|
|
} |
125
|
|
|
isset($this->contextBindings[$contextClass][$contextMethod]) |
126
|
|
|
|| ($this->contextBindings[$contextClass][$contextMethod] = []); |
127
|
|
|
$this->contextBindings[$contextClass][$contextMethod][$name] = $implementation; |
128
|
|
|
} |
129
|
|
|
return $this; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Share the service by given name |
134
|
|
|
* @param string $name |
135
|
|
|
* @return $this |
136
|
|
|
*/ |
137
|
|
|
public function share($name) |
138
|
|
|
{ |
139
|
|
|
$this->shares[$name] = null; |
140
|
|
|
return $this; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Add a definition to the container |
145
|
|
|
* ``` |
146
|
|
|
* //Add an instance like "instance" method |
147
|
|
|
* $container->set('student', new Student()); |
148
|
|
|
* |
149
|
|
|
* //Add a callable definition |
150
|
|
|
* $container->set('student', 'StudentFactory::create'); |
151
|
|
|
* $container->set('student', function(){ |
152
|
|
|
* return new Student(); |
153
|
|
|
* }); |
154
|
|
|
* |
155
|
|
|
* //Add an instance of "Slince\Di\Definition" |
156
|
|
|
* $container->set('student', new Definition('Foo\Bar\StudentClass', [ |
157
|
|
|
* 'gender' => 'boy', |
158
|
|
|
* 'school' => new Reference('school') |
159
|
|
|
* ], [ |
160
|
|
|
* 'setAge' => [18] |
161
|
|
|
* ], [ |
162
|
|
|
* 'father' => 'James', |
163
|
|
|
* 'mather' => 'Sophie' |
164
|
|
|
* ])); |
165
|
|
|
* |
166
|
|
|
* //Add a class definition |
167
|
|
|
* $container->set('student', Foo\Bar\StudentClass); |
168
|
|
|
* ``` |
169
|
|
|
* @param string $name |
170
|
|
|
* @param mixed $definition |
171
|
|
|
* @throws ConfigException |
172
|
|
|
* @return $this |
173
|
|
|
*/ |
174
|
|
|
public function set($name, $definition) |
175
|
|
|
{ |
176
|
|
|
if (is_callable($definition)) { |
177
|
|
|
$this->call($name, $definition); |
178
|
|
|
} elseif (is_object($definition)) { |
179
|
|
|
$this->instance($name, $definition); |
180
|
|
|
} elseif (is_string($definition)) { |
181
|
|
|
$this->define($name, $definition); |
182
|
|
|
} else { |
183
|
|
|
throw new ConfigException(sprintf("Unexpected object definition type '%s'", gettype($definition))); |
184
|
|
|
} |
185
|
|
|
return $this; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Get a service instance by specified name |
190
|
|
|
* @param string $name |
191
|
|
|
* @param array $arguments |
192
|
|
|
* @return object |
193
|
|
|
*/ |
194
|
|
|
public function get($name, $arguments = []) |
195
|
|
|
{ |
196
|
|
|
//If service is singleton, return instance directly. |
197
|
|
|
if (isset($this->shares[$name])) { |
198
|
|
|
return $this->shares[$name]; |
199
|
|
|
} |
200
|
|
|
//If there is no matching definition, creates an definition automatically |
201
|
|
|
if (!isset($this->definitions[$name])) { |
202
|
|
|
if (class_exists($name)) { |
203
|
|
|
$this->bind($name, $name); |
204
|
|
|
} else { |
205
|
|
|
throw new NotFoundException(sprintf('There is no definition for "%s"', $name)); |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
$instance = $this->createInstanceFromDefinition($this->definitions[$name], $arguments); |
209
|
|
|
//If the service be set as singleton mode, stores its instance |
210
|
|
|
if (array_key_exists($name, $this->shares)) { |
211
|
|
|
$this->shares[$name] = $instance; |
212
|
|
|
} |
213
|
|
|
return $instance; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* {@inheritdoc} |
218
|
|
|
*/ |
219
|
|
|
public function has($name) |
220
|
|
|
{ |
221
|
|
|
if (isset($this->shares[$name])) { |
222
|
|
|
return true; |
223
|
|
|
} |
224
|
|
|
if (!isset($this->definitions[$name]) && class_exists($name)) { |
225
|
|
|
$this->bind($name, $name); |
226
|
|
|
} |
227
|
|
|
return isset($this->definitions[$name]); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Gets all global parameters |
232
|
|
|
* @return array |
233
|
|
|
*/ |
234
|
|
|
public function getParameters() |
235
|
|
|
{ |
236
|
|
|
return $this->parameters->toArray(); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Sets array of parameters |
241
|
|
|
* @param array $parameterStore |
242
|
|
|
*/ |
243
|
|
|
public function setParameters(array $parameterStore) |
244
|
|
|
{ |
245
|
|
|
$this->parameters->setParameters($parameterStore); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Add some parameters |
250
|
|
|
* @param array $parameters |
251
|
|
|
*/ |
252
|
|
|
public function addParameters(array $parameters) |
253
|
|
|
{ |
254
|
|
|
$this->parameters->addParameters($parameters); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Sets a parameter with its name and value |
259
|
|
|
* @param $name |
260
|
|
|
* @param mixed $value |
261
|
|
|
*/ |
262
|
|
|
public function setParameter($name, $value) |
263
|
|
|
{ |
264
|
|
|
$this->parameters->setParameter($name, $value); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* Gets a parameter by given name |
269
|
|
|
* @param $name |
270
|
|
|
* @param mixed $default |
271
|
|
|
* @return mixed |
272
|
|
|
*/ |
273
|
|
|
public function getParameter($name, $default = null) |
274
|
|
|
{ |
275
|
|
|
return $this->parameters->getParameter($name, $default); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Resolves all arguments for the function or method. |
280
|
|
|
* @param \ReflectionFunctionAbstract $method |
281
|
|
|
* @param array $arguments |
282
|
|
|
* @param array $contextBindings The context bindings for the function |
283
|
|
|
* @throws DependencyInjectionException |
284
|
|
|
* @return array |
285
|
|
|
*/ |
286
|
|
|
public function resolveFunctionArguments(\ReflectionFunctionAbstract $method, array $arguments, array $contextBindings = []) |
287
|
|
|
{ |
288
|
|
|
$functionArguments = []; |
289
|
|
|
$arguments = $this->resolveParameters($arguments); |
290
|
|
|
//Checks whether the position is numeric |
291
|
|
|
$isNumeric = !empty($arguments) && is_numeric(key($arguments)); |
292
|
|
|
foreach ($method->getParameters() as $parameter) { |
293
|
|
|
$index = $isNumeric ? $parameter->getPosition() : $parameter->name; |
294
|
|
|
//If the dependency is provided directly |
295
|
|
|
if (isset($arguments[$index])) { |
296
|
|
|
$functionArguments[] = $arguments[$index]; |
297
|
|
|
} elseif (($dependency = $parameter->getClass()) != null) { |
298
|
|
|
$dependencyName = $dependency->name; |
299
|
|
|
//Use the new dependency if the dependency name has been replaced in array of context bindings |
300
|
|
|
isset($contextBindings[$dependencyName]) && $dependencyName = $contextBindings[$dependencyName]; |
301
|
|
|
try { |
302
|
|
|
$functionArguments[] = $this->get($dependencyName); |
303
|
|
|
} catch (DependencyInjectionException $exception) { |
304
|
|
|
if ($parameter->isOptional()) { |
305
|
|
|
$functionArguments[] = $parameter->getDefaultValue(); |
306
|
|
|
} else { |
307
|
|
|
throw $exception; |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
} elseif ($parameter->isOptional()) { |
311
|
|
|
$functionArguments[] = $parameter->getDefaultValue(); |
312
|
|
|
} else { |
313
|
|
|
throw new DependencyInjectionException(sprintf( |
314
|
|
|
'Missing required parameter "%s" when calling "%s"', |
315
|
|
|
$parameter->name, |
316
|
|
|
$method->getName() |
317
|
|
|
)); |
318
|
|
|
} |
319
|
|
|
} |
320
|
|
|
return $functionArguments; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* Gets all context bindings for the class and method |
325
|
|
|
* [ |
326
|
|
|
* 'User' => [ |
327
|
|
|
* 'original' => 'SchoolInterface' |
328
|
|
|
* 'bind' => 'MagicSchool', |
329
|
|
|
* ] |
330
|
|
|
* ] |
331
|
|
|
* @param string $contextClass |
332
|
|
|
* @param string $contextMethod |
333
|
|
|
* @return array |
334
|
|
|
*/ |
335
|
|
|
public function getContextBindings($contextClass, $contextMethod) |
336
|
|
|
{ |
337
|
|
|
if (!isset($this->contextBindings[$contextClass])) { |
338
|
|
|
return []; |
339
|
|
|
} |
340
|
|
|
$contextBindings = isset($this->contextBindings[$contextClass]['general']) |
341
|
|
|
? $this->contextBindings[$contextClass]['general'] : []; |
342
|
|
|
if (isset($this->contextBindings[$contextClass][$contextMethod])) { |
343
|
|
|
$contextBindings = array_merge($contextBindings, $this->contextBindings[$contextClass][$contextMethod]); |
344
|
|
|
} |
345
|
|
|
return $contextBindings; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
protected function createInstanceFromDefinition($definition, array $arguments) |
349
|
|
|
{ |
350
|
|
|
if (is_callable($definition)) { |
351
|
|
|
if ($arguments && ($definition instanceof \Closure || is_string($definition))) { |
|
|
|
|
352
|
|
|
$arguments['container'] = $this; |
353
|
|
|
$arguments = $this->resolveFunctionArguments( |
354
|
|
|
new \ReflectionFunction($definition), |
355
|
|
|
$arguments |
356
|
|
|
); |
357
|
|
|
} |
358
|
|
|
$arguments = $arguments ?: [$this]; |
359
|
|
|
$instance = call_user_func_array($definition, $arguments); |
360
|
|
|
} elseif ($definition instanceof ClassDefinition) { |
361
|
|
|
$instance = $this->getClassDefinitionResolver()->resolve($definition, $arguments); |
362
|
|
|
} elseif (is_object($definition)) { |
363
|
|
|
$instance = $definition; |
364
|
|
|
} else { |
365
|
|
|
throw new ConfigException(sprintf("Unexpected object definition type '%s'", gettype($definition))); |
366
|
|
|
} |
367
|
|
|
return $instance; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
protected function getClassDefinitionResolver() |
371
|
|
|
{ |
372
|
|
|
if (!is_null($this->classDefinitionResolver)) { |
373
|
|
|
return $this->classDefinitionResolver; |
374
|
|
|
} |
375
|
|
|
return $this->classDefinitionResolver = new ClassDefinitionResolver($this); |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* Resolves array of parameters |
380
|
|
|
* @param array $parameters |
381
|
|
|
* @return array |
382
|
|
|
*/ |
383
|
|
|
protected function resolveParameters($parameters) |
384
|
|
|
{ |
385
|
|
|
return array_map(function ($parameter) { |
386
|
|
|
if (is_string($parameter)) { |
387
|
|
|
$parameter = $this->formatParameter($parameter); |
388
|
|
|
} elseif ($parameter instanceof Reference) { |
389
|
|
|
$parameter = $this->get($parameter->getName()); |
390
|
|
|
} elseif (is_array($parameter)) { |
391
|
|
|
$parameter = $this->resolveParameters($parameter); |
392
|
|
|
} |
393
|
|
|
return $parameter; |
394
|
|
|
}, $parameters); |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* Formats parameter value |
399
|
|
|
* @param $value |
400
|
|
|
* @return mixed |
401
|
|
|
* @throws DependencyInjectionException |
402
|
|
|
*/ |
403
|
|
|
protected function formatParameter($value) |
404
|
|
|
{ |
405
|
|
|
//%xx% return the parameter |
406
|
|
|
if (preg_match("#^%([^%\s]+)%$#", $value, $match)) { |
407
|
|
|
$key = $match[1]; |
408
|
|
|
if ($parameter = $this->parameters->getParameter($key)) { |
409
|
|
|
return $parameter; |
410
|
|
|
} |
411
|
|
|
throw new DependencyInjectionException(sprintf("Parameter [%s] is not defined", $key)); |
412
|
|
|
} |
413
|
|
|
//"fool%bar%baz" |
414
|
|
|
return preg_replace_callback("#%([^%\s]+)%#", function ($matches) { |
415
|
|
|
$key = $matches[1]; |
416
|
|
|
if ($parameter = $this->parameters->getParameter($key)) { |
417
|
|
|
return $parameter; |
418
|
|
|
} |
419
|
|
|
throw new DependencyInjectionException(sprintf("Parameter [%s] is not defined", $key)); |
420
|
|
|
}, $value); |
421
|
|
|
} |
422
|
|
|
} |
423
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.