1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Spiral\Core; |
6
|
|
|
|
7
|
|
|
use Psr\Container\ContainerInterface; |
8
|
|
|
use ReflectionFunctionAbstract as ContextFunction; |
9
|
|
|
use Spiral\Core\Config\Alias; |
10
|
|
|
use Spiral\Core\Config\WeakReference; |
11
|
|
|
use Spiral\Core\Container\Autowire; |
12
|
|
|
use Spiral\Core\Container\InjectableInterface; |
13
|
|
|
use Spiral\Core\Container\SingletonInterface; |
14
|
|
|
use Spiral\Core\Exception\Container\ContainerException; |
15
|
|
|
use Spiral\Core\Exception\LogicException; |
16
|
|
|
use Spiral\Core\Exception\Scope\FinalizersException; |
17
|
|
|
use Spiral\Core\Exception\Scope\ScopeContainerLeakedException; |
18
|
|
|
use Spiral\Core\Internal\Common\DestructorTrait; |
19
|
|
|
use Spiral\Core\Internal\Config\StateBinder; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Auto-wiring container: declarative singletons, contextual injections, parent container |
23
|
|
|
* delegation and ability to lazy wire. |
24
|
|
|
* |
25
|
|
|
* Container does not support setter injections, private properties, etc. Normally it will work |
26
|
|
|
* with classes only to be as much invisible as possible. Attention, this is hungry implementation |
27
|
|
|
* of container, meaning it WILL try to resolve dependency unless you specified custom lazy |
28
|
|
|
* factory. |
29
|
|
|
* |
30
|
|
|
* You can use injectors to delegate class resolution to external container. |
31
|
|
|
* |
32
|
|
|
* @see InjectableInterface |
33
|
|
|
* @see SingletonInterface |
34
|
|
|
* |
35
|
|
|
* @psalm-import-type TResolver from BinderInterface |
36
|
|
|
* @psalm-import-type TInvokable from InvokerInterface |
37
|
|
|
*/ |
38
|
|
|
final class Container implements |
39
|
|
|
ContainerInterface, |
40
|
|
|
BinderInterface, |
41
|
|
|
FactoryInterface, |
42
|
|
|
ResolverInterface, |
43
|
|
|
InvokerInterface, |
44
|
|
|
ContainerScopeInterface, |
45
|
|
|
ScopeInterface |
|
|
|
|
46
|
|
|
{ |
47
|
|
|
use DestructorTrait; |
48
|
|
|
|
49
|
|
|
public const DEFAULT_ROOT_SCOPE_NAME = 'root'; |
50
|
|
|
|
51
|
|
|
private Internal\State $state; |
52
|
|
|
private ResolverInterface|Internal\Resolver $resolver; |
53
|
|
|
private FactoryInterface|Internal\Factory $factory; |
54
|
|
|
private ContainerInterface|Internal\Container $container; |
55
|
|
|
private BinderInterface|Internal\Binder $binder; |
56
|
|
|
private InvokerInterface|Internal\Invoker $invoker; |
57
|
|
|
private Internal\Scope $scope; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Container constructor. |
61
|
|
|
*/ |
62
|
1028 |
|
public function __construct( |
63
|
|
|
private Config $config = new Config(), |
64
|
|
|
?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME, |
65
|
|
|
) { |
66
|
1028 |
|
$this->initServices($this, $scopeName); |
67
|
|
|
|
68
|
|
|
/** @psalm-suppress RedundantPropertyInitializationCheck */ |
69
|
1028 |
|
\assert(isset($this->state)); |
70
|
|
|
|
71
|
|
|
// Bind himself |
72
|
1028 |
|
$shared = new Alias(self::class); |
73
|
1028 |
|
$this->state->bindings = \array_merge($this->state->bindings, [ |
74
|
1028 |
|
self::class => new WeakReference(\WeakReference::create($this)), |
75
|
1028 |
|
ContainerInterface::class => $shared, |
76
|
1028 |
|
BinderInterface::class => $shared, |
77
|
1028 |
|
FactoryInterface::class => $shared, |
78
|
1028 |
|
ContainerScopeInterface::class => $shared, |
79
|
1028 |
|
ScopeInterface::class => $shared, |
80
|
1028 |
|
ResolverInterface::class => $shared, |
81
|
1028 |
|
InvokerInterface::class => $shared, |
82
|
1028 |
|
]); |
83
|
|
|
} |
84
|
|
|
|
85
|
592 |
|
public function __destruct() |
86
|
|
|
{ |
87
|
592 |
|
$this->closeScope(); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Container can not be cloned. |
92
|
|
|
*/ |
93
|
1 |
|
public function __clone() |
94
|
|
|
{ |
95
|
1 |
|
throw new LogicException('Container is not cloneable.'); |
96
|
|
|
} |
97
|
|
|
|
98
|
442 |
|
public function resolveArguments( |
99
|
|
|
ContextFunction $reflection, |
100
|
|
|
array $parameters = [], |
101
|
|
|
bool $validate = true, |
102
|
|
|
): array { |
103
|
442 |
|
return $this->resolver->resolveArguments($reflection, $parameters, $validate); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
public function validateArguments(ContextFunction $reflection, array $arguments = []): void |
107
|
|
|
{ |
108
|
|
|
$this->resolver->validateArguments($reflection, $arguments); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @param string|null $context Related to parameter caused injection if any. |
113
|
|
|
* |
114
|
|
|
* @throws ContainerException |
115
|
|
|
* @throws \Throwable |
116
|
|
|
*/ |
117
|
541 |
|
public function make(string $alias, array $parameters = [], string $context = null): mixed |
118
|
|
|
{ |
119
|
|
|
/** @psalm-suppress TooManyArguments */ |
120
|
541 |
|
return $this->factory->make($alias, $parameters, $context); |
|
|
|
|
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Context parameter will be passed to class injectors, which makes possible to use this method |
125
|
|
|
* as: |
126
|
|
|
* |
127
|
|
|
* $this->container->get(DatabaseInterface::class, 'default'); |
128
|
|
|
* |
129
|
|
|
* Attention, context ignored when outer container has instance by alias. |
130
|
|
|
* |
131
|
|
|
* @template T |
132
|
|
|
* |
133
|
|
|
* @param class-string<T>|string|Autowire $id |
|
|
|
|
134
|
|
|
* @param string|null $context Call context. |
135
|
|
|
* |
136
|
|
|
* @return ($id is class-string ? T : mixed) |
|
|
|
|
137
|
|
|
* |
138
|
|
|
* @throws ContainerException |
139
|
|
|
* @throws \Throwable |
140
|
|
|
*/ |
141
|
802 |
|
public function get(string|Autowire $id, string $context = null): mixed |
142
|
|
|
{ |
143
|
|
|
/** @psalm-suppress TooManyArguments */ |
144
|
802 |
|
return $this->container->get($id, $context); |
|
|
|
|
145
|
|
|
} |
146
|
|
|
|
147
|
485 |
|
public function has(string $id): bool |
148
|
|
|
{ |
149
|
485 |
|
return $this->container->has($id); |
150
|
|
|
} |
151
|
|
|
|
152
|
3 |
|
public function getBinder(?string $scope = null): BinderInterface |
153
|
|
|
{ |
154
|
3 |
|
return $scope === null |
155
|
|
|
? $this->binder |
156
|
3 |
|
: new StateBinder($this->config->scopedBindings->getState($scope)); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* @throws \Throwable |
161
|
|
|
* |
162
|
|
|
* @deprecated use {@see runScoped()} instead. |
163
|
|
|
*/ |
164
|
558 |
|
public function runScope(array $bindings, callable $scope): mixed |
165
|
|
|
{ |
166
|
558 |
|
$binds = &$this->state->bindings; |
167
|
558 |
|
$singletons = &$this->state->singletons; |
168
|
558 |
|
$cleanup = $previous = $prevSin = []; |
169
|
558 |
|
foreach ($bindings as $alias => $resolver) { |
170
|
|
|
// Store previous bindings |
171
|
557 |
|
if (isset($binds[$alias])) { |
172
|
510 |
|
$previous[$alias] = $binds[$alias]; |
173
|
|
|
} else { |
174
|
|
|
// Store bindings to be removed |
175
|
556 |
|
$cleanup[] = $alias; |
176
|
|
|
} |
177
|
|
|
// Store previous singletons |
178
|
557 |
|
if (isset($singletons[$alias])) { |
179
|
40 |
|
$prevSin[$alias] = $singletons[$alias]; |
180
|
40 |
|
unset($singletons[$alias]); |
181
|
|
|
} |
182
|
|
|
|
183
|
557 |
|
$this->binder->bind($alias, $resolver); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
try { |
187
|
558 |
|
return ContainerScope::getContainer() !== $this |
188
|
557 |
|
? ContainerScope::runScope($this, $scope) |
189
|
544 |
|
: $scope($this); |
190
|
|
|
} finally { |
191
|
|
|
// Remove new bindings |
192
|
558 |
|
foreach ($cleanup as $alias) { |
193
|
556 |
|
unset($binds[$alias], $singletons[$alias]); |
194
|
|
|
} |
195
|
|
|
// Restore previous bindings |
196
|
558 |
|
foreach ($previous as $alias => $resolver) { |
197
|
510 |
|
$binds[$alias] = $resolver; |
198
|
|
|
} |
199
|
|
|
// Restore singletons |
200
|
558 |
|
foreach ($prevSin as $alias => $instance) { |
201
|
40 |
|
$singletons[$alias] = $instance; |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Invoke given closure or function withing specific IoC scope. |
208
|
|
|
*/ |
209
|
23 |
|
public function runScoped(callable $closure, array $bindings = [], ?string $name = null, bool $autowire = true): mixed |
210
|
|
|
{ |
211
|
|
|
// Open scope |
212
|
23 |
|
$container = new self($this->config, $name); |
213
|
|
|
|
214
|
|
|
try { |
215
|
|
|
// Configure scope |
216
|
23 |
|
$container->scope->setParent($this, $this->scope); |
217
|
|
|
|
218
|
|
|
// Add specific bindings |
219
|
23 |
|
foreach ($bindings as $alias => $resolver) { |
220
|
12 |
|
$container->binder->bind($alias, $resolver); |
221
|
|
|
} |
222
|
|
|
|
223
|
23 |
|
return ContainerScope::runScope( |
224
|
23 |
|
$container, |
225
|
23 |
|
static function (self $container) use ($autowire, $closure): mixed { |
226
|
|
|
try { |
227
|
23 |
|
return $autowire |
228
|
23 |
|
? $container->invoke($closure) |
229
|
22 |
|
: $closure($container); |
230
|
|
|
} finally { |
231
|
23 |
|
$container->closeScope(); |
232
|
|
|
} |
233
|
23 |
|
} |
234
|
23 |
|
); |
235
|
|
|
} finally { |
236
|
|
|
// Check the container has not been leaked |
237
|
23 |
|
$link = \WeakReference::create($container); |
238
|
23 |
|
unset($container); |
239
|
23 |
|
if ($link->get() !== null) { |
240
|
23 |
|
throw new ScopeContainerLeakedException($name, $this->scope->getParentScopeNames()); |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Get current scope container. |
247
|
|
|
* |
248
|
|
|
* @internal it might be removed in the future. |
249
|
|
|
*/ |
250
|
|
|
public function getCurrentContainer(): ContainerInterface |
251
|
|
|
{ |
252
|
|
|
return ContainerScope::getContainer() ?? $this; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Bind value resolver to container alias. Resolver can be class name (will be constructed |
257
|
|
|
* for each method call), function array or Closure (executed every call). Only object resolvers |
258
|
|
|
* supported by this method. |
259
|
|
|
*/ |
260
|
808 |
|
public function bind(string $alias, mixed $resolver): void |
261
|
|
|
{ |
262
|
808 |
|
$this->binder->bind($alias, $resolver); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Bind value resolver to container alias to be executed as cached. Resolver can be class name |
267
|
|
|
* (will be constructed only once), function array or Closure (executed only once call). |
268
|
|
|
* |
269
|
|
|
* @psalm-param TResolver $resolver |
270
|
|
|
*/ |
271
|
603 |
|
public function bindSingleton(string $alias, string|array|callable|object $resolver): void |
272
|
|
|
{ |
273
|
603 |
|
$this->binder->bindSingleton($alias, $resolver); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Check if alias points to constructed instance (singleton). |
278
|
|
|
*/ |
279
|
9 |
|
public function hasInstance(string $alias): bool |
280
|
|
|
{ |
281
|
9 |
|
return $this->binder->hasInstance($alias); |
282
|
|
|
} |
283
|
|
|
|
284
|
2 |
|
public function removeBinding(string $alias): void |
285
|
|
|
{ |
286
|
2 |
|
$this->binder->removeBinding($alias); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* @psalm-param TInvokable $target |
291
|
|
|
*/ |
292
|
576 |
|
public function invoke(mixed $target, array $parameters = []): mixed |
293
|
|
|
{ |
294
|
576 |
|
return $this->invoker->invoke($target, $parameters); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Bind class or class interface to the injector source (InjectorInterface). |
299
|
|
|
*/ |
300
|
374 |
|
public function bindInjector(string $class, string $injector): void |
301
|
|
|
{ |
302
|
374 |
|
$this->binder->bindInjector($class, $injector); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
public function removeInjector(string $class): void |
306
|
|
|
{ |
307
|
|
|
$this->binder->removeInjector($class); |
308
|
|
|
} |
309
|
|
|
|
310
|
8 |
|
public function hasInjector(string $class): bool |
311
|
|
|
{ |
312
|
8 |
|
return $this->binder->hasInjector($class); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Init internal container services. |
317
|
|
|
*/ |
318
|
1028 |
|
private function initServices( |
319
|
|
|
self $container, |
320
|
|
|
?string $scopeName, |
321
|
|
|
): void { |
322
|
1028 |
|
$isRoot = $container->config->lockRoot(); |
323
|
|
|
|
324
|
|
|
// Get named scope or create anonymous one |
325
|
1028 |
|
$state = match (true) { |
326
|
1028 |
|
$scopeName === null => new Internal\State(), |
327
|
|
|
// Only root container can make default bindings directly |
328
|
1028 |
|
$isRoot => $container->config->scopedBindings->getState($scopeName), |
|
|
|
|
329
|
1028 |
|
default => clone $container->config->scopedBindings->getState($scopeName), |
330
|
1028 |
|
}; |
331
|
|
|
|
332
|
1028 |
|
$constructor = new Internal\Common\Registry($container->config, [ |
333
|
1028 |
|
'state' => $state, |
334
|
1028 |
|
'scope' => new Internal\Scope($scopeName), |
335
|
1028 |
|
]); |
336
|
|
|
|
337
|
|
|
// Create container services |
338
|
1028 |
|
foreach ($container->config as $property => $class) { |
339
|
1028 |
|
if (\property_exists($container, $property)) { |
340
|
1028 |
|
$container->$property = $constructor->get($property, $class); |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Execute finalizers and destruct the container. |
347
|
|
|
* |
348
|
|
|
* @throws FinalizersException |
349
|
|
|
*/ |
350
|
592 |
|
private function closeScope(): void |
351
|
|
|
{ |
352
|
|
|
/** @psalm-suppress RedundantPropertyInitializationCheck */ |
353
|
592 |
|
if (!isset($this->scope)) { |
354
|
23 |
|
$this->destruct(); |
355
|
23 |
|
return; |
356
|
|
|
} |
357
|
|
|
|
358
|
592 |
|
$scopeName = $this->scope->getScopeName(); |
359
|
|
|
|
360
|
|
|
// Run finalizers |
361
|
592 |
|
$errors = []; |
362
|
592 |
|
foreach ($this->state->finalizers as $finalizer) { |
363
|
|
|
try { |
364
|
4 |
|
$this->invoker->invoke($finalizer); |
365
|
|
|
} catch (\Throwable $e) { |
366
|
|
|
$errors[] = $e; |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
// Destroy the container |
371
|
592 |
|
$this->destruct(); |
372
|
|
|
|
373
|
|
|
// Throw collected errors |
374
|
592 |
|
if ($errors !== []) { |
375
|
|
|
throw new FinalizersException($scopeName, $errors); |
376
|
|
|
} |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
This interface has been deprecated. The supplier of the interface has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.