1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Codeburner Framework. |
5
|
|
|
* |
6
|
|
|
* @author Alex Rohleder <[email protected]> |
7
|
|
|
* @copyright 2015 Alex Rohleder |
8
|
|
|
* @license http://opensource.org/licenses/MIT |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace Codeburner\Container; |
12
|
|
|
|
13
|
|
|
use Closure; |
14
|
|
|
use Exception; |
15
|
|
|
use ReflectionClass; |
16
|
|
|
use ReflectionFunction; |
17
|
|
|
use ReflectionParameter; |
18
|
|
|
use Psr\Container\ContainerInterface; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* The container class is reponsable to construct all objects |
22
|
|
|
* of the project automatically, with total abstraction of dependencies. |
23
|
|
|
* |
24
|
|
|
* @author Alex Rohleder <[email protected]> |
25
|
|
|
* @version 1.0.0 |
26
|
|
|
* @since 0.0.1 |
27
|
|
|
*/ |
28
|
|
|
|
29
|
|
|
class Container implements ContainerInterface |
30
|
|
|
{ |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Holds all resolved or resolvable instances into the container. |
34
|
|
|
* |
35
|
|
|
* @var array |
36
|
|
|
*/ |
37
|
|
|
|
38
|
|
|
protected $collection; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Class specific defined dependencies. |
42
|
|
|
* |
43
|
|
|
* @var array |
44
|
|
|
*/ |
45
|
|
|
|
46
|
|
|
protected $dependencies; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Cache of classes inspector and resolver. |
50
|
|
|
* |
51
|
|
|
* @var array |
52
|
|
|
*/ |
53
|
|
|
|
54
|
|
|
protected $resolving; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Cache of classes dependencies in callbacks ready for resolution. |
58
|
|
|
* |
59
|
|
|
* @var array |
60
|
|
|
*/ |
61
|
|
|
|
62
|
|
|
protected $resolved; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Call a user function injecting the dependencies. |
66
|
|
|
* |
67
|
|
|
* @param string|Closure $function The function or the user function name. |
68
|
|
|
* @param array $parameters The predefined dependencies. |
69
|
|
|
* |
70
|
|
|
* @return mixed |
71
|
|
|
*/ |
72
|
|
|
|
73
|
|
|
public function call($function, array $parameters = []) |
74
|
|
|
{ |
75
|
|
|
$inspector = new ReflectionFunction($function); |
76
|
|
|
$dependencies = $inspector->getParameters(); |
77
|
|
|
$resolvedClosureDependencies = []; |
78
|
|
|
|
79
|
|
|
foreach ($dependencies as $dependency) { |
80
|
|
|
if (isset($parameters[$dependency->name])) { |
81
|
1 |
|
$resolvedClosureDependencies[] = $parameters[$dependency->name]; |
82
|
|
|
} else { |
83
|
1 |
|
if (($class = $dependency->getClass()) === null) { |
84
|
1 |
|
$resolvedClosureDependencies[] = $dependency->isOptional() ? $dependency->getDefaultValue() : null; |
85
|
1 |
|
} else $resolvedClosureDependencies[] = $this->make($class->name); |
86
|
1 |
|
} |
87
|
1 |
|
} |
88
|
|
|
|
89
|
|
|
return call_user_func_array($function, $resolvedClosureDependencies); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Makes an element or class injecting automatically all the dependencies. |
94
|
|
|
* |
95
|
|
|
* @param string $abstract The class name or container element name to make. |
96
|
|
|
* @param array $parameters Specific parameters definition. |
97
|
|
|
* @param bool $force Specify if a new element must be given and the dependencies must have be recalculated. |
98
|
3 |
|
* |
99
|
|
|
* @throws \Psr\Container\Exception\ContainerException |
100
|
3 |
|
* @return object|null |
101
|
3 |
|
*/ |
102
|
3 |
|
|
103
|
|
|
public function make($abstract, array $parameters = [], bool $force = false) |
104
|
3 |
|
{ |
105
|
2 |
|
if ($force === false && isset($this->collection[$abstract])) { |
106
|
1 |
|
return $this->get($abstract); |
107
|
1 |
|
} |
108
|
1 |
|
|
109
|
|
|
if (isset($this->resolving[$abstract])) { |
110
|
1 |
|
return $this->resolving[$abstract]($abstract, $parameters); |
111
|
|
|
} |
112
|
3 |
|
|
113
|
|
|
try { |
114
|
3 |
|
$callable = $this->resolving[$abstract] = $this->construct($abstract, $force); |
115
|
|
|
return $callable($abstract, $parameters); |
116
|
|
|
} catch (Exception $e) { |
117
|
|
|
throw new Exceptions\ContainerException($e->getMessage()); |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Construct a class and all the dependencies using the reflection library of PHP. |
123
|
|
|
* |
124
|
|
|
* @param string $abstract The class name or container element name to make. |
125
|
|
|
* @param bool $force Specify if a new element must be given and the dependencies must have be recalculated. |
126
|
|
|
* |
127
|
|
|
* @throws ReflectionException |
128
|
19 |
|
* @return Closure |
129
|
|
|
*/ |
130
|
19 |
|
|
131
|
2 |
|
protected function construct(string $abstract, bool $force) : Closure |
132
|
|
|
{ |
133
|
|
|
$inspector = new ReflectionClass($abstract); |
134
|
19 |
|
|
135
|
4 |
|
if ($constructor = $inspector->getConstructor()) { |
136
|
|
|
$dependencies = $constructor->getParameters(); |
137
|
|
|
|
138
|
19 |
|
return function ($abstract, $parameters) use ($inspector, $dependencies, $force) { |
139
|
18 |
|
$resolvedClassParameters = []; |
140
|
|
|
|
141
|
|
|
foreach ($dependencies as $dependency) { |
142
|
|
|
if (isset($parameters[$dependency->name])) { |
143
|
|
|
$resolvedClassParameters[] = $parameters[$dependency->name]; |
144
|
|
|
} else $resolvedClassParameters[] = $this->resolve($abstract, $dependency, $force); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
return $inspector->newInstanceArgs($resolvedClassParameters); |
148
|
|
|
}; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
return function ($abstract) { |
152
|
19 |
|
return new $abstract; |
153
|
|
|
}; |
154
|
19 |
|
} |
155
|
|
|
|
156
|
18 |
|
/** |
157
|
8 |
|
* Resolve all the given class reflected dependencies. |
158
|
|
|
* |
159
|
|
|
* @param string $abstract The class name or container element name to resolve dependencies. |
160
|
8 |
|
* @param ReflectionParameter $dependency The class dependency to be resolved. |
161
|
|
|
* @param bool $force Specify if the dependencies must be recalculated. |
162
|
8 |
|
* |
163
|
8 |
|
* @return Object |
164
|
|
|
*/ |
165
|
8 |
|
|
166
|
8 |
|
protected function resolve(string $abstract, ReflectionParameter $dependency, bool $force) |
167
|
|
|
{ |
168
|
8 |
|
$key = $abstract.$dependency->name; |
169
|
8 |
|
|
170
|
|
|
if (!isset($this->resolved[$key]) || $force === true) { |
171
|
|
|
$this->resolved[$key] = $this->generate($abstract, $dependency); |
172
|
|
|
} |
173
|
15 |
|
|
174
|
15 |
|
return $this->resolved[$key]($this); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Generate the dependencies callbacks to jump some conditions in every dependency creation. |
179
|
|
|
* |
180
|
|
|
* @param string $abstract The class name or container element name to resolve dependencies. |
181
|
|
|
* @param ReflectionParameter $dependency The class dependency to be resolved. |
182
|
|
|
* |
183
|
|
|
* @return Closure |
184
|
|
|
*/ |
185
|
|
|
|
186
|
|
|
protected function generate(string $abstract, ReflectionParameter $dependency) : Closure |
187
|
8 |
|
{ |
188
|
|
|
if ($class = $dependency->getClass()) { |
189
|
8 |
|
$classname = $class->name; |
190
|
|
|
$key = $abstract.$classname; |
191
|
8 |
|
|
192
|
8 |
|
if (isset($this->dependencies[$key])) { |
193
|
8 |
|
return $this->dependencies[$key]; |
194
|
|
|
} |
195
|
8 |
|
|
196
|
|
|
return function () use ($classname) { |
197
|
|
|
return $this->make($classname); |
198
|
|
|
}; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
return $class->getDefaultValue(); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Reset the container, removing all the elements, cache and options. |
206
|
|
|
* |
207
|
8 |
|
* @return void |
208
|
|
|
*/ |
209
|
8 |
|
|
210
|
8 |
|
public function flush() |
211
|
8 |
|
{ |
212
|
|
|
$this->collection = []; |
213
|
8 |
|
$this->dependencies = []; |
214
|
3 |
|
$this->resolving = []; |
215
|
5 |
|
$this->resolved = []; |
216
|
5 |
|
} |
217
|
5 |
|
|
218
|
|
|
/** |
219
|
|
|
* Finds an entry of the container by its identifier and returns it. |
220
|
|
|
* |
221
|
|
|
* @param string $abstract Identifier of the entry to look for. |
222
|
|
|
* |
223
|
|
|
* @throws \Psr\Container\Exception\NotFoundException No entry was found for this identifier. |
224
|
|
|
* @throws \Psr\Container\Exception\ContainerException Error while retrieving the entry. |
225
|
|
|
* |
226
|
|
|
* @return mixed Entry. |
227
|
|
|
*/ |
228
|
|
View Code Duplication |
public function get($abstract) |
|
|
|
|
229
|
|
|
{ |
230
|
|
|
if (! isset($this->collection[$abstract])) { |
231
|
|
|
throw new Exceptions\NotFoundException("Element '$abstract' not found"); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
if ($this->collection[$abstract] instanceof Closure) { |
235
|
|
|
try { |
236
|
|
|
return $this->collection[$abstract]($this); |
237
|
|
|
} catch (Exception $e) { |
238
|
|
|
throw new Exceptions\ContainerException($e->getMessage()); |
239
|
|
|
} |
240
|
|
|
} |
241
|
|
|
|
242
|
10 |
|
return $this->collection[$abstract]; |
243
|
|
|
} |
244
|
10 |
|
|
245
|
8 |
|
/** |
246
|
|
|
* Returns true if the container can return an entry for the given identifier. |
247
|
8 |
|
* Returns false otherwise. |
248
|
2 |
|
* |
249
|
6 |
|
* `has($abstract)` returning true does not mean that `get($abstract)` will not throw an exception. |
250
|
|
|
* It does however mean that `get($abstract)` will not throw a `NotFoundException`. |
251
|
|
|
* |
252
|
2 |
|
* @param string $abstract Identifier of the entry to look for. |
253
|
|
|
* |
254
|
|
|
* @return boolean |
255
|
2 |
|
*/ |
256
|
|
|
|
257
|
2 |
|
public function has($abstract) |
258
|
2 |
|
{ |
259
|
2 |
|
return isset($this->collection[$abstract]); |
260
|
2 |
|
} |
261
|
|
|
|
262
|
2 |
|
/** |
263
|
|
|
* Verify if an element has a singleton instance. |
264
|
2 |
|
* |
265
|
|
|
* @param string The class name or container element name to resolve dependencies. |
266
|
|
|
* @return bool |
267
|
2 |
|
*/ |
268
|
|
|
|
269
|
2 |
|
public function isSingleton(string $abstract) : bool |
270
|
2 |
|
{ |
271
|
|
|
return isset($this->collection[$abstract]); |
272
|
2 |
|
} |
273
|
|
|
|
274
|
2 |
|
/** |
275
|
|
|
* Bind a new element to the container. |
276
|
|
|
* |
277
|
1 |
|
* @param string $abstract The alias name that will be used to call the element. |
278
|
|
|
* @param string|closure $concrete The element class name, or an closure that makes the element. |
279
|
1 |
|
* @param bool $shared Define if the element will be a singleton instance. |
280
|
1 |
|
* |
281
|
|
|
* @return \Codeburner\Container\Container |
282
|
1 |
|
*/ |
283
|
|
|
|
284
|
1 |
|
public function set(string $abstract, $concrete, bool $shared = false) : self |
285
|
1 |
|
{ |
286
|
|
|
if ($concrete instanceof Closure === false) { |
287
|
1 |
|
$concrete = function (Container $container) use ($concrete) { |
288
|
|
|
return $container->make($concrete); |
289
|
1 |
|
}; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
if ($shared === true) { |
293
|
|
|
$this->collection[$abstract] = $concrete($this); |
294
|
|
|
} else $this->collection[$abstract] = $concrete; |
295
|
|
|
|
296
|
|
|
return $this; |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* Bind a new element to the container IF the element name not exists in the container. |
301
|
|
|
* |
302
|
|
|
* @param string $abstract The alias name that will be used to call the element. |
303
|
|
|
* @param string|closure $concrete The element class name, or an closure that makes the element. |
304
|
|
|
* @param bool $shared Define if the element will be a singleton instance. |
305
|
|
|
* |
306
|
|
|
* @return \Codeburner\Container\Container |
307
|
|
|
*/ |
308
|
|
|
|
309
|
|
|
public function setIf(string $abstract, $concrete, bool $shared = false) : self |
310
|
|
|
{ |
311
|
|
|
if (! isset($this->collection[$abstract])) { |
312
|
|
|
$this->set($abstract, $concrete, $shared); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
return $this; |
316
|
2 |
|
} |
317
|
|
|
|
318
|
2 |
|
/** |
319
|
|
|
* Bind an specific instance to a class dependency. |
320
|
|
|
* |
321
|
|
|
* @param string $class The class full name. |
322
|
|
|
* @param string $dependencyName The dependency full name. |
323
|
|
|
* @param string|closure $dependency The specific object class name or a classure that makes the element. |
324
|
|
|
* |
325
|
|
|
* @return \Codeburner\Container\Container |
326
|
|
|
*/ |
327
|
5 |
|
|
328
|
|
|
public function setTo(string $class, string $dependencyName, $dependency) : self |
329
|
5 |
|
{ |
330
|
|
|
if ($dependency instanceof Closure === false) { |
331
|
|
|
if (is_object($dependency)) { |
332
|
|
|
$dependency = function () use ($dependency) { |
333
|
|
|
return $dependency; |
334
|
|
|
}; |
335
|
|
|
} else { |
336
|
|
|
$dependency = function () use ($dependency) { |
337
|
|
|
return $this->get($dependency); |
338
|
|
|
}; |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
|
342
|
12 |
|
$this->dependencies[$class.$dependencyName] = $dependency; |
343
|
|
|
|
344
|
12 |
|
return $this; |
345
|
|
|
} |
346
|
8 |
|
|
347
|
11 |
|
/** |
348
|
11 |
|
* Bind an element that will be construct only one time, and every call for the element, |
349
|
|
|
* the same instance will be given. |
350
|
12 |
|
* |
351
|
6 |
|
* @param string $abstract The alias name that will be used to call the element. |
352
|
12 |
|
* @param string|closure $concrete The element class name, or an closure that makes the element. |
353
|
|
|
* |
354
|
12 |
|
* @return \Codeburner\Container\Container |
355
|
|
|
*/ |
356
|
|
|
|
357
|
|
|
public function singleton(string $abstract, $concrete) : self |
358
|
|
|
{ |
359
|
|
|
$this->set($abstract, $concrete, true); |
360
|
|
|
|
361
|
|
|
return $this; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* Bind an object to the container. |
366
|
|
|
* |
367
|
1 |
|
* @param string $abstract The alias name that will be used to call the object. |
368
|
|
|
* @param object $instance The object that will be inserted. |
369
|
1 |
|
* |
370
|
1 |
|
* @throws \Psr\Container\Exception\ContainerException When $instance is not an object. |
371
|
1 |
|
* @return \Codeburner\Container\Container |
372
|
|
|
*/ |
373
|
1 |
|
|
374
|
|
|
public function instance(string $abstract, $instance) : self |
375
|
|
|
{ |
376
|
|
|
if (! is_object($instance)) { |
377
|
|
|
$type = gettype($instance); |
378
|
|
|
throw new Exceptions\ContainerException("Trying to store {$type} as object."); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
$this->collection[$abstract] = $instance; |
382
|
|
|
|
383
|
|
|
return $this; |
384
|
|
|
} |
385
|
|
|
|
386
|
3 |
|
/** |
387
|
|
|
* Modify an element with a given function that receive the old element as argument. |
388
|
3 |
|
* |
389
|
2 |
|
* @param string $abstract The alias name that will be used to call the element. |
390
|
|
|
* @param closure $extension The function that receives the old element and return a new or modified one. |
391
|
1 |
|
* |
392
|
1 |
|
* @throws \Psr\Container\Exception\NotFoundException When no element was found with $abstract key. |
393
|
1 |
|
* @return \Codeburner\Container\Container |
394
|
|
|
*/ |
395
|
1 |
|
|
396
|
1 |
|
public function extend(string $abstract, closure $extension) : self |
397
|
|
|
{ |
398
|
2 |
|
if (! isset($this->collection[$abstract])) { |
399
|
|
|
throw new Exceptions\NotFoundException; |
400
|
3 |
|
} |
401
|
|
|
|
402
|
3 |
|
$object = $this->collection[$abstract]; |
403
|
|
|
|
404
|
|
|
if ($object instanceof Closure) { |
405
|
|
|
$this->collection[$abstract] = function () use ($object, $extension) { |
406
|
|
|
return $extension($object($this), $this); |
407
|
|
|
}; |
408
|
|
|
} else { |
409
|
|
|
$this->collection[$abstract] = $extension($object, $this); |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
return $this; |
413
|
|
|
} |
414
|
|
|
|
415
|
3 |
|
/** |
416
|
|
|
* Makes an resolvable element an singleton. |
417
|
3 |
|
* |
418
|
|
|
* @param string $abstract The alias name that will be used to call the element. |
419
|
3 |
|
* |
420
|
|
|
* @throws \Psr\Container\Exception\NotFoundException When no element was found with $abstract key. |
421
|
|
|
* @throws \Psr\Container\Exception\ContainerException When the element on $abstract key is not resolvable. |
422
|
|
|
* |
423
|
|
|
* @return \Codeburner\Container\Container |
424
|
|
|
*/ |
425
|
|
|
|
426
|
|
View Code Duplication |
public function share(string $abstract) : self |
|
|
|
|
427
|
|
|
{ |
428
|
|
|
if (! isset($this->collection[$abstract])) { |
429
|
|
|
throw new Exceptions\NotFoundException; |
430
|
|
|
} |
431
|
3 |
|
|
432
|
|
|
if (! $this->collection[$abstract] instanceof Closure) { |
433
|
3 |
|
throw new Exceptions\ContainerException("'$abstract' must be a resolvable element"); |
434
|
3 |
|
} |
435
|
3 |
|
|
436
|
|
|
$this->collection[$abstract] = $this->collection[$abstract]($this); |
437
|
3 |
|
|
438
|
|
|
return $this; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
} |
442
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.