1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Phossa Project |
4
|
|
|
* |
5
|
|
|
* PHP version 5.4 |
6
|
|
|
* |
7
|
|
|
* @category Library |
8
|
|
|
* @package Phossa2\Di |
9
|
|
|
* @copyright Copyright (c) 2016 phossa.com |
10
|
|
|
* @license http://mit-license.org/ MIT License |
11
|
|
|
* @link http://www.phossa.com/ |
12
|
|
|
*/ |
13
|
|
|
/*# declare(strict_types=1); */ |
14
|
|
|
|
15
|
|
|
namespace Phossa2\Di\Traits; |
16
|
|
|
|
17
|
|
|
use Phossa2\Di\Message\Message; |
18
|
|
|
use Phossa2\Di\Exception\LogicException; |
19
|
|
|
use Phossa2\Di\Exception\NotFoundException; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* FactoryTrait |
23
|
|
|
* |
24
|
|
|
* Create service instance here |
25
|
|
|
* |
26
|
|
|
* @package Phossa2\Di |
27
|
|
|
* @author Hong Zhang <[email protected]> |
28
|
|
|
* @version 2.0.0 |
29
|
|
|
* @since 2.0.0 added |
30
|
|
|
*/ |
31
|
|
|
trait FactoryTrait |
32
|
|
|
{ |
33
|
|
|
use ScopeTrait; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* for loop detection |
37
|
|
|
* |
38
|
|
|
* @var array |
39
|
|
|
* @access protected |
40
|
|
|
*/ |
41
|
|
|
protected $loop = []; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Full scope info |
45
|
|
|
* |
46
|
|
|
* @param sting $id |
47
|
|
|
* @return array |
48
|
|
|
* @access protected |
49
|
|
|
*/ |
50
|
|
|
protected function fullScopeInfo(/*# string */ $id)/*# : array */ |
51
|
|
|
{ |
52
|
|
|
list($rawId, $scope) = $this->scopedInfo($id); |
53
|
|
|
|
54
|
|
|
// special treatment if $scope is a '#service_id' |
55
|
|
|
if (isset($this->loop[$scope])) { |
56
|
|
|
$scope .= '_' . $this->loop[$scope]; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
return [$rawId, $this->scopedId($rawId, $scope), $scope]; |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Create the instance with loop detection |
64
|
|
|
* |
65
|
|
|
* @param string $rawId |
66
|
|
|
* @param array $args arguments for the constructor if any |
67
|
|
|
* @return object |
68
|
|
|
* @throws LogicException if instantiation goes wrong or loop detected |
69
|
|
|
* @access protected |
70
|
|
|
*/ |
71
|
|
|
protected function createInstance(/*# string */ $rawId, array $args) |
72
|
|
|
{ |
73
|
|
|
static $counter = 0; |
74
|
|
|
|
75
|
|
|
// conver 'service_id' to '#service_id' |
76
|
|
|
$serviceId = $this->getServiceId($rawId); |
77
|
|
|
|
78
|
|
|
// loop detected |
79
|
|
|
if (isset($this->loop[$serviceId])) { |
80
|
|
|
throw new LogicException( |
81
|
|
|
Message::get(Message::DI_LOOP_DETECTED, $rawId), |
82
|
|
|
Message::DI_LOOP_DETECTED |
83
|
|
|
); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
// set loop marker |
87
|
|
|
$this->loop[$serviceId] = ++$counter; |
88
|
|
|
|
89
|
|
|
// create the service instance |
90
|
|
|
$obj = $this->createFromId($rawId, $args); |
91
|
|
|
|
92
|
|
|
// remove current marker |
93
|
|
|
unset($this->loop[$serviceId]); |
94
|
|
|
|
95
|
|
|
return $obj; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Create object base on the raw id |
100
|
|
|
* |
101
|
|
|
* @param string $rawId |
102
|
|
|
* @param array $arguments |
103
|
|
|
* @return object |
104
|
|
|
* @throws LogicException if instantiation goes wrong |
105
|
|
|
* @access protected |
106
|
|
|
*/ |
107
|
|
|
protected function createFromId(/*# string */ $rawId, array $arguments) |
108
|
|
|
{ |
109
|
|
|
// get definition |
110
|
|
|
$def = $this->getDefinition($rawId, $arguments); |
111
|
|
|
|
112
|
|
|
if (is_string($def['class'])) { |
113
|
|
|
// classname |
114
|
|
|
$obj = $this->constructObject($def['class'], $def['args']); |
115
|
|
|
|
116
|
|
|
} else { |
117
|
|
|
// object or callable etc. |
118
|
|
|
$obj = $this->executeCallable($def['class'], $def['args']); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
// after creation |
122
|
|
|
$this->afterCreation($obj, $def); |
123
|
|
|
|
124
|
|
|
return $obj; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Get service definition |
129
|
|
|
* |
130
|
|
|
* @param string $rawId |
131
|
|
|
* @param array $args |
132
|
|
|
* @return array |
133
|
|
|
* @access protected |
134
|
|
|
*/ |
135
|
|
|
protected function getDefinition( |
136
|
|
|
/*# string */ $rawId, |
137
|
|
|
array $args |
138
|
|
|
)/*# : array */ { |
139
|
|
|
// get the definition |
140
|
|
|
$def = $this->getResolver()->getService($rawId); |
141
|
|
|
|
142
|
|
|
// fix class |
143
|
|
|
if (!is_array($def)) { |
144
|
|
|
$def = ['class' => $def]; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
// fix arguments |
148
|
|
|
if (!empty($args) || !isset($def['args'])) { |
149
|
|
|
// resolve external arguments |
150
|
|
|
$this->resolve($args); |
151
|
|
|
|
152
|
|
|
$def['args'] = $args; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
return $def; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Instantiate service object from classname |
160
|
|
|
* |
161
|
|
|
* @param string $class |
162
|
|
|
* @param array $args |
163
|
|
|
* @return object |
164
|
|
|
* @throws LogicException if something goes wrong |
165
|
|
|
* @access protected |
166
|
|
|
*/ |
167
|
|
|
protected function constructObject(/*# string */ $class, array $args) |
168
|
|
|
{ |
169
|
|
|
$reflector = new \ReflectionClass($class); |
170
|
|
|
$constructor = $reflector->getConstructor(); |
171
|
|
|
|
172
|
|
|
// not constructor defined |
173
|
|
|
if (is_null($constructor)) { |
174
|
|
|
$obj = $reflector->newInstanceWithoutConstructor(); |
175
|
|
|
|
176
|
|
|
// normal class with constructor |
177
|
|
|
} else { |
178
|
|
|
$args = $this->matchArguments( |
179
|
|
|
$constructor->getParameters(), |
180
|
|
|
$args |
181
|
|
|
); |
182
|
|
|
$obj = $reflector->newInstanceArgs($args); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
return $obj; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Execute a (pseudo) callable with arguments |
190
|
|
|
* |
191
|
|
|
* @param callable|array|object $callable callable or pseudo callable |
192
|
|
|
* @param array $arguments |
193
|
|
|
* @return mixed |
194
|
|
|
* @throws LogicException if something goes wrong |
195
|
|
|
* @access protected |
196
|
|
|
*/ |
197
|
|
|
protected function executeCallable($callable, array $arguments = []) |
198
|
|
|
{ |
199
|
|
|
// not callable |
200
|
|
|
if (!is_callable($callable)) { |
201
|
|
|
return $callable; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
if (!empty($arguments)) { |
205
|
|
|
$args = $this->matchCallableArguments($callable, $arguments); |
206
|
|
|
return call_user_func_array($callable, $args); |
207
|
|
|
} else { |
208
|
|
|
return call_user_func($callable); |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Rebuild callable base methodName and object |
214
|
|
|
* |
215
|
|
|
* method: |
216
|
|
|
* - ['function', [ arguments...]] |
217
|
|
|
* |
218
|
|
|
* - [ callable, [ arguments ...]] |
219
|
|
|
* |
220
|
|
|
* - ['method', [ arguments ...]] |
221
|
|
|
* convert to [[$object, 'method'], [ ... ]] |
222
|
|
|
* |
223
|
|
|
* @param mixed method |
224
|
|
|
* @param object|null $object to construct callable |
225
|
|
|
* @throws LogicException if something goes wrong |
226
|
|
|
* @access protected |
227
|
|
|
*/ |
228
|
|
|
protected function executeMethod($method, $object = null) |
229
|
|
|
{ |
230
|
|
|
$callable = $method[0]; |
231
|
|
|
$arguments = isset($method[1]) ? $method[1] : []; |
232
|
|
|
|
233
|
|
|
// rebuild callable from $object |
234
|
|
|
if (null !== $object) { |
235
|
|
|
$callable = [$object, $callable]; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
$this->executeCallable($callable, $arguments); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Matching callable arguments |
243
|
|
|
* |
244
|
|
|
* @param callable $callable |
245
|
|
|
* @param array $arguments |
246
|
|
|
* @return array the matched arguments |
247
|
|
|
* @throws LogicException if something goes wrong |
248
|
|
|
* @access protected |
249
|
|
|
*/ |
250
|
|
|
protected function matchCallableArguments( |
251
|
|
|
callable $callable, |
252
|
|
|
array $arguments |
253
|
|
|
)/*# : array */ { |
254
|
|
|
// array type |
255
|
|
|
if (is_array($callable)) { |
256
|
|
|
$reflector = new \ReflectionClass($callable[0]); |
257
|
|
|
$method = $reflector->getMethod($callable[1]); |
258
|
|
|
|
259
|
|
|
} elseif ($this->isInvocable($callable)) { |
260
|
|
|
$reflector = new \ReflectionClass($callable); |
261
|
|
|
$method = $reflector->getMethod('__invoke'); |
262
|
|
|
|
263
|
|
|
// simple function |
264
|
|
|
} else { |
265
|
|
|
$method = new \ReflectionFunction($callable); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
return $this->matchArguments( |
269
|
|
|
$method->getParameters(), |
270
|
|
|
$arguments |
271
|
|
|
); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Match provided arguments with a method/function's reflection parameters |
276
|
|
|
* |
277
|
|
|
* @param \ReflectionParameter[] $reflectionParameters |
278
|
|
|
* @param array $providedArguments |
279
|
|
|
* @return array the resolved arguments |
280
|
|
|
* @throws LogicException |
281
|
|
|
* @throws NotFoundException |
282
|
|
|
* @access protected |
283
|
|
|
*/ |
284
|
|
|
protected function matchArguments( |
285
|
|
|
array $reflectionParameters, |
286
|
|
|
array $providedArguments |
287
|
|
|
)/*# : array */ { |
288
|
|
|
// result |
289
|
|
|
$resolvedArguments = []; |
290
|
|
|
|
291
|
|
|
// go thru each predefined parameter |
292
|
|
|
foreach ($reflectionParameters as $i => $param) { |
293
|
|
|
// arg to match with |
294
|
|
|
$argument = isset($providedArguments[0]) ? $providedArguments[0] : null; |
295
|
|
|
|
296
|
|
|
// $param is an interface or class ? |
|
|
|
|
297
|
|
|
$class = $param->getClass(); |
298
|
|
|
|
299
|
|
|
if ($this->isTypeMatched($param, $argument, $class)) { |
300
|
|
|
// type matched |
301
|
|
|
$resolvedArguments[$i] = array_shift($providedArguments); |
302
|
|
|
|
303
|
|
|
} elseif (null !== $class) { |
304
|
|
|
// not matched, but $param is an interface or class |
305
|
|
|
$resolvedArguments[$i] = $this->getObjectByClass($class->getName()); |
306
|
|
|
|
307
|
|
|
} elseif ($param->isOptional()) { |
308
|
|
|
// $param is optional, $arg is null |
309
|
|
|
break; |
310
|
|
|
} else { |
311
|
|
|
throw new LogicException( |
312
|
|
|
Message::get(Message::DI_PARAMETER_NOTFOUND, $param->getName()), |
313
|
|
|
Message::DI_PARAMETER_NOTFOUND |
314
|
|
|
); |
315
|
|
|
} |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
// append remained arguments if any |
319
|
|
|
if (!empty($providedArguments)) { |
320
|
|
|
$resolvedArguments = array_merge($resolvedArguments, $providedArguments); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
return $resolvedArguments; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* Is $parameter same type as the $argument ? |
328
|
|
|
* |
329
|
|
|
* @param \ReflectionParameter $parameter |
|
|
|
|
330
|
|
|
* @param mixed $argument |
331
|
|
|
* @param null|string $class |
332
|
|
|
* @return bool |
333
|
|
|
* @throws LogicException if type missmatch |
334
|
|
|
* @access protected |
335
|
|
|
*/ |
336
|
|
|
protected function isTypeMatched( |
337
|
|
|
\ReflectionParameter $parameters, |
338
|
|
|
$argument, |
339
|
|
|
$class |
340
|
|
|
)/*# : bool */ { |
341
|
|
|
if (null === $argument) { |
342
|
|
|
return false; |
343
|
|
|
} elseif (null !== $class) { |
344
|
|
|
return is_a($argument, $parameters->getClass()->getName()); |
345
|
|
|
} else { |
346
|
|
|
return true; |
347
|
|
|
} |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Get an object base on provided classname or interface name |
352
|
|
|
* |
353
|
|
|
* @param string $classname class or interface name |
354
|
|
|
* @return object |
355
|
|
|
* @throws \Exception if something goes wrong |
356
|
|
|
* @access protected |
357
|
|
|
*/ |
358
|
|
|
protected function getObjectByClass(/*# string */ $classname) |
359
|
|
|
{ |
360
|
|
|
// mapping exists |
361
|
|
|
if ($this->getResolver()->hasMapping($classname)) { |
362
|
|
|
$classname = $this->getResolver()->getMapping($classname); |
363
|
|
|
if (is_object($classname)) { |
364
|
|
|
return $classname; |
365
|
|
|
} |
366
|
|
|
} |
367
|
|
|
return $this->get($classname); |
|
|
|
|
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* Is $var an object with '__invoke()' defined |
372
|
|
|
* |
373
|
|
|
* @param mixed $var |
374
|
|
|
* @return bool |
375
|
|
|
* @access protected |
376
|
|
|
*/ |
377
|
|
|
protected function isInvocable($var)/*# : bool */ |
378
|
|
|
{ |
379
|
|
|
return is_object($var) && method_exists($var, '__invoke'); |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Things to do after object created. |
384
|
|
|
* |
385
|
|
|
* @param object $object |
386
|
|
|
* @param array $definition service definition for $object |
387
|
|
|
* @access protected |
388
|
|
|
*/ |
389
|
|
|
protected function afterCreation($object, array $definition) |
390
|
|
|
{ |
391
|
|
|
// execute methods from this object |
392
|
|
|
if (isset($definition['methods'])) { |
393
|
|
|
foreach ($definition['methods'] as $method) { |
394
|
|
|
$this->executeMethod($method, $object); |
395
|
|
|
} |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
// execute common methods in 'di.common' for all created objects |
399
|
|
|
if ($this->getResolver()->has('', 'common')) { |
400
|
|
|
$methods = $this->mergeNodeInfo($this->getResolver()->get('', 'common')); |
401
|
|
|
$this->executeTester($object, $methods); |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Execute [tester, todo] pairs, both use $object as argument |
407
|
|
|
* |
408
|
|
|
* signatures |
409
|
|
|
* |
410
|
|
|
* - tester: function($object) { return $object instance of XXXX; } |
411
|
|
|
* - todoer: function($object, $container) { } |
412
|
|
|
* |
413
|
|
|
* @param object $object |
414
|
|
|
* @param array $methods |
415
|
|
|
* @access protected |
416
|
|
|
*/ |
417
|
|
|
protected function executeTester($object, array $methods) |
418
|
|
|
{ |
419
|
|
|
foreach ($methods as $method) { |
420
|
|
|
// tester: $method[0] |
|
|
|
|
421
|
|
|
if ($method[0]($object)) { |
422
|
|
|
// todoer: $method[1] |
|
|
|
|
423
|
|
|
$method[1]($object, $this); |
424
|
|
|
} |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* Merge data in the node, normally merge methods |
430
|
|
|
* |
431
|
|
|
* @param array $nodeData |
432
|
|
|
* @return array |
433
|
|
|
* @access protected |
434
|
|
|
*/ |
435
|
|
|
protected function mergeNodeInfo(array $nodeData)/*# : array */ |
436
|
|
|
{ |
437
|
|
|
// no merge |
438
|
|
|
if (isset($nodeData[0])) { |
439
|
|
|
return $nodeData; |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
// in sections |
443
|
|
|
$result = []; |
444
|
|
|
foreach ($nodeData as $data) { |
445
|
|
|
$result = array_merge($result, $data); |
446
|
|
|
} |
447
|
|
|
return $result; |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* Append '#' to rawId, representing a service object id |
452
|
|
|
* |
453
|
|
|
* @param string $rawId |
454
|
|
|
* @return string |
455
|
|
|
* @access protected |
456
|
|
|
*/ |
457
|
|
|
protected function getServiceId(/*# string */ $rawId)/*# : string */ |
458
|
|
|
{ |
459
|
|
|
return '#' . $rawId; |
460
|
|
|
} |
461
|
|
|
} |
462
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.