1
|
|
|
<?php declare(strict_types = 1); |
2
|
|
|
|
3
|
|
|
namespace Venta\Container; |
4
|
|
|
|
5
|
|
|
use Closure; |
6
|
|
|
use InvalidArgumentException; |
7
|
|
|
use ReflectionClass; |
8
|
|
|
use Venta\Container\Exception\ArgumentResolverException; |
9
|
|
|
use Venta\Container\Exception\CircularReferenceException; |
10
|
|
|
use Venta\Container\Exception\NotFoundException; |
11
|
|
|
use Venta\Container\Exception\UninstantiableServiceException; |
12
|
|
|
use Venta\Container\Exception\UnresolvableDependencyException; |
13
|
|
|
use Venta\Contracts\Container\Container as ContainerContract; |
14
|
|
|
use Venta\Contracts\Container\Invoker as InvokerContract; |
15
|
|
|
use Venta\Contracts\Container\ObjectInflector as ObjectInflectorContract; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Class Container |
19
|
|
|
* |
20
|
|
|
* @package Venta\Container |
21
|
|
|
*/ |
22
|
|
|
class Container implements ContainerContract |
23
|
|
|
{ |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Array of callable definitions. |
27
|
|
|
* |
28
|
|
|
* @var Invokable[] |
29
|
|
|
*/ |
30
|
|
|
private $callableDefinitions = []; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Array of class definitions. |
34
|
|
|
* |
35
|
|
|
* @var string[] |
36
|
|
|
*/ |
37
|
|
|
private $classDefinitions = []; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Array of decorator definitions. |
41
|
|
|
* |
42
|
|
|
* @var callable[][] |
43
|
|
|
*/ |
44
|
|
|
private $decoratorDefinitions = []; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Array of container service callable factories. |
48
|
|
|
* |
49
|
|
|
* @var Closure[] |
50
|
|
|
*/ |
51
|
|
|
private $factories = []; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var ObjectInflectorContract |
55
|
|
|
*/ |
56
|
|
|
private $inflector; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Array of resolved instances. |
60
|
|
|
* |
61
|
|
|
* @var object[] |
62
|
|
|
*/ |
63
|
|
|
private $instances = []; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var InvokerContract |
67
|
|
|
*/ |
68
|
|
|
private $invoker; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Array of container service identifiers. |
72
|
|
|
* |
73
|
|
|
* @var string[] |
74
|
|
|
*/ |
75
|
|
|
private $keys = []; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Array of container service identifiers currently being resolved. |
79
|
|
|
* |
80
|
|
|
* @var string[] |
81
|
|
|
*/ |
82
|
|
|
private $resolving = []; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Array of instances identifiers marked as shared. |
86
|
|
|
* Such instances will be instantiated once and returned on consecutive gets. |
87
|
|
|
* |
88
|
|
|
* @var bool[] |
89
|
|
|
*/ |
90
|
|
|
private $shared = []; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Container constructor. |
94
|
|
|
*/ |
95
|
44 |
|
public function __construct() |
96
|
|
|
{ |
97
|
44 |
|
$argumentResolver = new ArgumentResolver($this); |
98
|
44 |
|
$this->setInvoker(new Invoker($this, $argumentResolver)); |
99
|
44 |
|
$this->setObjectInflector(new ObjectInflector($argumentResolver)); |
100
|
44 |
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* @inheritDoc |
104
|
|
|
*/ |
105
|
4 |
|
public function addInflection(string $id, string $method, array $arguments = []) |
106
|
|
|
{ |
107
|
4 |
|
$this->validateId($id); |
108
|
4 |
|
$this->inflector->addInflection($this->normalize($id), $method, $arguments); |
109
|
3 |
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @inheritDoc |
113
|
|
|
*/ |
114
|
7 |
|
public function bindClass(string $id, string $class, $shared = false) |
115
|
|
|
{ |
116
|
7 |
|
if (!$this->isResolvableService($class)) { |
117
|
1 |
|
throw new InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); |
118
|
|
|
} |
119
|
|
|
$this->register($id, $shared, function ($id) use ($class) { |
120
|
5 |
|
$this->classDefinitions[$id] = $class; |
121
|
6 |
|
}); |
122
|
5 |
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @inheritDoc |
126
|
|
|
*/ |
127
|
12 |
|
public function bindFactory(string $id, $callable, $shared = false) |
128
|
|
|
{ |
129
|
12 |
|
$reflectedCallable = new Invokable($callable); |
130
|
12 |
|
if (!$this->isResolvableCallable($reflectedCallable)) { |
131
|
|
|
throw new InvalidArgumentException('Invalid callable provided.'); |
132
|
|
|
} |
133
|
|
|
|
134
|
12 |
|
$this->register( |
135
|
|
|
$id, |
136
|
|
|
$shared, |
137
|
|
|
function ($id) use ($reflectedCallable) { |
138
|
12 |
|
$this->callableDefinitions[$id] = $reflectedCallable; |
139
|
12 |
|
}); |
140
|
12 |
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @inheritDoc |
144
|
|
|
*/ |
145
|
4 |
|
public function bindInstance(string $id, $instance) |
146
|
|
|
{ |
147
|
4 |
|
if (!$this->isConcrete($instance)) { |
148
|
|
|
throw new InvalidArgumentException('Invalid instance provided.'); |
149
|
|
|
} |
150
|
|
|
$this->register($id, true, function ($id) use ($instance) { |
151
|
4 |
|
$this->instances[$id] = $instance; |
152
|
4 |
|
}); |
153
|
4 |
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @inheritDoc |
157
|
|
|
* @param callable|string $callable Callable to call OR class name to instantiate and invoke. |
158
|
|
|
*/ |
159
|
12 |
|
public function call($callable, array $arguments = []) |
160
|
|
|
{ |
161
|
12 |
|
return $this->invoker->call($callable, $arguments); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* @inheritDoc |
166
|
|
|
*/ |
167
|
3 |
|
public function decorate($id, callable $callback) |
168
|
|
|
{ |
169
|
3 |
|
$id = $this->normalize($id); |
170
|
|
|
|
171
|
|
|
// Check if correct id is provided. |
172
|
3 |
|
if (!$this->isResolvableService($id)) { |
173
|
|
|
throw new InvalidArgumentException('Invalid id provided.'); |
174
|
|
|
} |
175
|
|
|
|
176
|
3 |
|
$this->decoratorDefinitions[$id][] = $callback; |
177
|
3 |
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* @inheritDoc |
181
|
|
|
*/ |
182
|
35 |
|
public function get($id, array $arguments = []) |
183
|
|
|
{ |
184
|
35 |
|
$id = $this->normalize($id); |
185
|
|
|
// We try to resolve alias first to get a real service id. |
186
|
35 |
|
if (!$this->isResolvableService($id)) { |
187
|
2 |
|
throw new NotFoundException($id, $this->resolving); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
// Look up service in resolved instances first. |
191
|
33 |
View Code Duplication |
if (isset($this->instances[$id])) { |
|
|
|
|
192
|
5 |
|
$object = $this->decorateObject($id, $this->instances[$id]); |
193
|
|
|
// Delete all decorator callbacks to avoid applying them once more on another get call. |
194
|
5 |
|
unset($this->decoratorDefinitions[$id]); |
195
|
|
|
|
196
|
5 |
|
return $object; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
// Detect circular references. |
200
|
|
|
// We mark service as being resolved to detect circular references through out the resolution chain. |
201
|
29 |
|
if (isset($this->resolving[$id])) { |
202
|
3 |
|
throw new CircularReferenceException($id, $this->resolving); |
203
|
|
|
} else { |
204
|
29 |
|
$this->resolving[$id] = $id; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
try { |
208
|
|
|
// Instantiate service and apply inflections. |
209
|
29 |
|
$object = $this->instantiateService($id, $arguments); |
210
|
25 |
|
$this->inflector->applyInflections($object); |
211
|
24 |
|
$object = $this->decorateObject($id, $object); |
212
|
|
|
|
213
|
|
|
// Cache shared instances. |
214
|
24 |
View Code Duplication |
if (isset($this->shared[$id])) { |
|
|
|
|
215
|
1 |
|
$this->instances[$id] = $object; |
216
|
|
|
// Remove all decorator callbacks to prevent further decorations on concrete instance. |
217
|
1 |
|
unset($this->decoratorDefinitions[$id]); |
218
|
|
|
} |
219
|
|
|
|
220
|
24 |
|
return $object; |
221
|
5 |
|
} catch (ArgumentResolverException $resolveException) { |
222
|
1 |
|
throw new UnresolvableDependencyException($id, $this->resolving, $resolveException); |
223
|
|
|
} finally { |
224
|
29 |
|
unset($this->resolving[$id]); |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* @inheritDoc |
230
|
|
|
*/ |
231
|
23 |
|
public function has($id): bool |
232
|
|
|
{ |
233
|
23 |
|
return $this->isResolvableService($this->normalize($id)); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* @inheritDoc |
238
|
|
|
*/ |
239
|
1 |
|
public function isCallable($callable): bool |
240
|
|
|
{ |
241
|
1 |
|
return $this->invoker->isCallable($callable); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* @param InvokerContract $invoker |
246
|
|
|
* @return void |
247
|
|
|
*/ |
248
|
44 |
|
protected function setInvoker(InvokerContract $invoker) |
249
|
|
|
{ |
250
|
44 |
|
$this->invoker = $invoker; |
251
|
44 |
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* @param ObjectInflectorContract $inflector |
255
|
|
|
* @return void |
256
|
|
|
*/ |
257
|
44 |
|
protected function setObjectInflector(ObjectInflectorContract $inflector) |
258
|
|
|
{ |
259
|
44 |
|
$this->inflector = $inflector; |
260
|
44 |
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Forbid container cloning. |
264
|
|
|
* |
265
|
|
|
* @codeCoverageIgnore |
266
|
|
|
*/ |
267
|
|
|
private function __clone() |
268
|
|
|
{ |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Create callable factory with resolved arguments from callable. |
273
|
|
|
* |
274
|
|
|
* @param Invokable $invokable |
275
|
|
|
* @return Closure |
276
|
|
|
*/ |
277
|
12 |
|
private function createServiceFactoryFromCallable(Invokable $invokable): Closure |
278
|
|
|
{ |
279
|
|
|
return function (array $arguments = []) use ($invokable) { |
280
|
12 |
|
return $this->invoker->invoke($invokable, $arguments); |
281
|
12 |
|
}; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Create callable factory with resolved arguments from class name. |
286
|
|
|
* |
287
|
|
|
* @param string $class |
288
|
|
|
* @return Closure |
289
|
|
|
* @throws UninstantiableServiceException |
290
|
|
|
*/ |
291
|
22 |
|
private function createServiceFactoryFromClass(string $class): Closure |
292
|
|
|
{ |
293
|
22 |
|
$reflection = new ReflectionClass($class); |
294
|
22 |
|
if (!$reflection->isInstantiable()) { |
295
|
1 |
|
throw new UninstantiableServiceException($class, $this->resolving); |
296
|
|
|
} |
297
|
21 |
|
$constructor = $reflection->getConstructor(); |
298
|
|
|
|
299
|
21 |
|
if ($constructor && $constructor->getNumberOfParameters() > 0) { |
300
|
15 |
|
$invokable = new Invokable($constructor); |
301
|
|
|
|
302
|
|
|
return function (array $arguments = []) use ($invokable) { |
303
|
15 |
|
return $this->invoker->invoke($invokable, $arguments); |
304
|
15 |
|
}; |
305
|
|
|
} |
306
|
|
|
|
307
|
19 |
|
return function () use ($class) { |
308
|
19 |
|
return new $class(); |
309
|
19 |
|
}; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Applies decoration callbacks to provided instance. |
314
|
|
|
* |
315
|
|
|
* @param string $id |
316
|
|
|
* @param $object |
317
|
|
|
* @return object |
318
|
|
|
*/ |
319
|
28 |
|
private function decorateObject(string $id, $object) |
320
|
|
|
{ |
321
|
28 |
|
if (isset($this->decoratorDefinitions[$id])) { |
322
|
3 |
|
foreach ($this->decoratorDefinitions[$id] as $callback) { |
323
|
3 |
|
$object = $this->invoker->call($callback, [$object]); |
324
|
3 |
|
$this->inflector->applyInflections($object); |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
|
328
|
28 |
|
return $object; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* Create callable factory for the subject service. |
333
|
|
|
* |
334
|
|
|
* @param string $id |
335
|
|
|
* @param array $arguments |
336
|
|
|
* @return mixed |
337
|
|
|
*/ |
338
|
29 |
|
private function instantiateService(string $id, array $arguments) |
339
|
|
|
{ |
340
|
29 |
|
if (isset($this->instances[$id])) { |
341
|
|
|
return $this->instances[$id]; |
342
|
|
|
} |
343
|
|
|
|
344
|
29 |
|
if (!isset($this->factories[$id])) { |
345
|
29 |
|
if (isset($this->callableDefinitions[$id])) { |
346
|
12 |
|
$this->factories[$id] = $this->createServiceFactoryFromCallable($this->callableDefinitions[$id]); |
347
|
22 |
|
} elseif (isset($this->classDefinitions[$id]) && $this->classDefinitions[$id] !== $id) { |
348
|
|
|
// Recursive call allows to bind contract to contract. |
349
|
3 |
|
return $this->instantiateService($this->classDefinitions[$id], $arguments); |
350
|
|
|
} else { |
351
|
22 |
|
$this->factories[$id] = $this->createServiceFactoryFromClass($id); |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
355
|
28 |
|
return ($this->factories[$id])($arguments); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Check if subject service is an object instance. |
360
|
|
|
* |
361
|
|
|
* @param mixed $service |
362
|
|
|
* @return bool |
363
|
|
|
*/ |
364
|
4 |
|
private function isConcrete($service): bool |
365
|
|
|
{ |
366
|
4 |
|
return is_object($service) && !$service instanceof Closure; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Verifies that provided callable can be called by service container. |
371
|
|
|
* |
372
|
|
|
* @param Invokable $reflectedCallable |
373
|
|
|
* @return bool |
374
|
|
|
*/ |
375
|
12 |
|
private function isResolvableCallable(Invokable $reflectedCallable): bool |
376
|
|
|
{ |
377
|
|
|
// If array represents callable we need to be sure it's an object or a resolvable service id. |
378
|
12 |
|
$callable = $reflectedCallable->callable(); |
379
|
|
|
|
380
|
12 |
|
return $reflectedCallable->isFunction() |
381
|
6 |
|
|| is_object($callable[0]) |
382
|
12 |
|
|| $this->isResolvableService($callable[0]); |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* Check if container can resolve the service with subject identifier. |
387
|
|
|
* |
388
|
|
|
* @param string $id |
389
|
|
|
* @return bool |
390
|
|
|
*/ |
391
|
39 |
|
private function isResolvableService(string $id): bool |
392
|
|
|
{ |
393
|
39 |
|
return isset($this->keys[$id]) || class_exists($id); |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* Normalize key to use across container. |
398
|
|
|
* |
399
|
|
|
* @param string $id |
400
|
|
|
* @return string |
401
|
|
|
*/ |
402
|
38 |
|
private function normalize(string $id): string |
403
|
|
|
{ |
404
|
38 |
|
return ltrim($id, '\\'); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
/** |
408
|
|
|
* Registers binding. |
409
|
|
|
* After this method call binding can be resolved by container. |
410
|
|
|
* |
411
|
|
|
* @param string $id |
412
|
|
|
* @param bool $shared |
413
|
|
|
* @param Closure $registrationCallback |
414
|
|
|
* @return void |
415
|
|
|
*/ |
416
|
22 |
|
private function register(string $id, bool $shared, Closure $registrationCallback) |
417
|
|
|
{ |
418
|
|
|
// Check if correct service is provided. |
419
|
22 |
|
$this->validateId($id); |
420
|
21 |
|
$id = $this->normalize($id); |
421
|
|
|
|
422
|
|
|
// Clean up previous bindings, if any. |
423
|
21 |
|
unset($this->instances[$id], $this->shared[$id], $this->keys[$id]); |
424
|
|
|
|
425
|
|
|
// Register service with provided callback. |
426
|
21 |
|
$registrationCallback($id); |
427
|
|
|
|
428
|
|
|
// Mark service as shared when needed. |
429
|
21 |
|
$this->shared[$id] = $shared ?: null; |
430
|
|
|
|
431
|
|
|
// Save service key to make it recognizable by container. |
432
|
21 |
|
$this->keys[$id] = true; |
433
|
21 |
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* Validate service identifier. Throw an Exception in case of invalid value. |
437
|
|
|
* |
438
|
|
|
* @param string $id |
439
|
|
|
* @return void |
440
|
|
|
* @throws InvalidArgumentException |
441
|
|
|
*/ |
442
|
26 |
|
private function validateId(string $id) |
443
|
|
|
{ |
444
|
26 |
|
if (!interface_exists($id) && !class_exists($id)) { |
445
|
1 |
|
throw new InvalidArgumentException(sprintf( |
446
|
1 |
|
'Invalid service id "%s". Service id must be an existing interface or class name.', $id |
447
|
|
|
) |
448
|
|
|
); |
449
|
|
|
} |
450
|
25 |
|
} |
451
|
|
|
} |
452
|
|
|
|
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.