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