1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Yiisoft\Di; |
6
|
|
|
|
7
|
|
|
use Closure; |
8
|
|
|
use Psr\Container\ContainerExceptionInterface; |
9
|
|
|
use Psr\Container\ContainerInterface; |
10
|
|
|
use Psr\Container\NotFoundExceptionInterface; |
11
|
|
|
use Throwable; |
12
|
|
|
use Traversable; |
13
|
|
|
use Yiisoft\Definitions\ArrayDefinition; |
14
|
|
|
use Yiisoft\Definitions\DefinitionStorage; |
15
|
|
|
use Yiisoft\Definitions\Exception\CircularReferenceException; |
16
|
|
|
use Yiisoft\Definitions\Exception\InvalidConfigException; |
17
|
|
|
use Yiisoft\Definitions\Exception\NotInstantiableException; |
18
|
|
|
use Yiisoft\Definitions\Helpers\DefinitionValidator; |
19
|
|
|
use Yiisoft\Di\Helpers\DefinitionNormalizer; |
20
|
|
|
use Yiisoft\Di\Helpers\DefinitionParser; |
21
|
|
|
use Yiisoft\Di\Helpers\TagHelper; |
22
|
|
|
|
23
|
|
|
use function array_key_exists; |
24
|
|
|
use function array_keys; |
25
|
|
|
use function implode; |
26
|
|
|
use function in_array; |
27
|
|
|
use function is_array; |
28
|
|
|
use function is_callable; |
29
|
|
|
use function is_object; |
30
|
|
|
use function is_string; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container. |
34
|
|
|
*/ |
35
|
|
|
final class Container implements ContainerInterface |
36
|
|
|
{ |
37
|
|
|
private const META_TAGS = 'tags'; |
38
|
|
|
private const META_RESET = 'reset'; |
39
|
|
|
private const ALLOWED_META = [self::META_TAGS, self::META_RESET]; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var DefinitionStorage Storage of object definitions. |
43
|
|
|
*/ |
44
|
|
|
private DefinitionStorage $definitions; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @var array Used to collect IDs of objects instantiated during build |
48
|
|
|
* to detect circular references. |
49
|
|
|
*/ |
50
|
|
|
private array $building = []; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @var bool $validate If definitions should be validated. |
54
|
|
|
*/ |
55
|
|
|
private bool $validate; |
56
|
|
|
|
57
|
|
|
private array $instances = []; |
58
|
|
|
|
59
|
|
|
private CompositeContainer $delegates; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var array Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`. |
63
|
|
|
* @psalm-var array<string, iterable<string>> |
64
|
|
|
*/ |
65
|
|
|
private array $tags; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* @var Closure[] |
69
|
|
|
*/ |
70
|
|
|
private array $resetters = []; |
71
|
|
|
private bool $useResettersFromMeta = true; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @param ContainerConfigInterface $config Container configuration. |
75
|
|
|
* |
76
|
|
|
* @throws InvalidConfigException If configuration is not valid. |
77
|
|
|
*/ |
78
|
138 |
|
public function __construct(ContainerConfigInterface $config) |
79
|
|
|
{ |
80
|
138 |
|
$this->definitions = new DefinitionStorage( |
81
|
|
|
[ |
82
|
|
|
ContainerInterface::class => $this, |
83
|
|
|
StateResetter::class => StateResetter::class, |
84
|
|
|
], |
85
|
138 |
|
$config->useStrictMode() |
86
|
|
|
); |
87
|
138 |
|
$this->validate = $config->shouldValidate(); |
88
|
138 |
|
$this->setTags($config->getTags()); |
89
|
135 |
|
$this->addDefinitions($config->getDefinitions()); |
90
|
125 |
|
$this->addProviders($config->getProviders()); |
91
|
119 |
|
$this->setDelegates($config->getDelegates()); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Returns a value indicating whether the container has the definition of the specified name. |
96
|
|
|
* |
97
|
|
|
* @param string $id Class name, interface name or alias name. |
98
|
|
|
* |
99
|
|
|
* @return bool Whether the container is able to provide instance of class specified. |
100
|
|
|
* |
101
|
|
|
* @see addDefinition() |
102
|
|
|
*/ |
103
|
47 |
|
public function has(string $id): bool |
104
|
|
|
{ |
105
|
47 |
|
if (TagHelper::isTagAlias($id)) { |
106
|
3 |
|
$tag = TagHelper::extractTagFromAlias($id); |
107
|
3 |
|
return isset($this->tags[$tag]); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
try { |
111
|
44 |
|
return $this->definitions->has($id); |
112
|
3 |
|
} catch (CircularReferenceException) { |
113
|
3 |
|
return true; |
114
|
|
|
} |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Returns an instance by either interface name or alias. |
119
|
|
|
* |
120
|
|
|
* Same instance of the class will be returned each time this method is called. |
121
|
|
|
* |
122
|
|
|
* @param string $id The interface or an alias name that was previously registered. |
123
|
|
|
* |
124
|
|
|
* @throws CircularReferenceException |
125
|
|
|
* @throws InvalidConfigException |
126
|
|
|
* @throws NotFoundExceptionInterface |
127
|
|
|
* @throws NotInstantiableException |
128
|
|
|
* @throws BuildingException |
129
|
|
|
* |
130
|
|
|
* @return mixed|object An instance of the requested interface. |
131
|
|
|
* |
132
|
|
|
* @psalm-template T |
133
|
|
|
* @psalm-param string|class-string<T> $id |
134
|
|
|
* @psalm-return ($id is class-string ? T : mixed) |
135
|
|
|
*/ |
136
|
119 |
|
public function get(string $id) |
137
|
|
|
{ |
138
|
119 |
|
if (!array_key_exists($id, $this->instances)) { |
139
|
|
|
try { |
140
|
|
|
try { |
141
|
119 |
|
$this->instances[$id] = $this->build($id); |
142
|
20 |
|
} catch (NotFoundExceptionInterface $e) { |
143
|
11 |
|
if (!$this->delegates->has($id)) { |
144
|
8 |
|
throw $e; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** @psalm-suppress MixedReturnStatement */ |
148
|
119 |
|
return $this->delegates->get($id); |
149
|
|
|
} |
150
|
17 |
|
} catch (Throwable $e) { |
151
|
17 |
|
if ($e instanceof ContainerExceptionInterface && !$e instanceof InvalidConfigException) { |
152
|
15 |
|
throw $e; |
153
|
|
|
} |
154
|
2 |
|
throw new BuildingException($id, $e, $this->definitions->getBuildStack(), $e); |
155
|
|
|
} |
156
|
|
|
} |
157
|
|
|
|
158
|
119 |
|
if ($id === StateResetter::class) { |
159
|
10 |
|
$delegatesResetter = null; |
160
|
10 |
|
if ($this->delegates->has(StateResetter::class)) { |
161
|
2 |
|
$delegatesResetter = $this->delegates->get(StateResetter::class); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** @var StateResetter $mainResetter */ |
165
|
10 |
|
$mainResetter = $this->instances[$id]; |
166
|
|
|
|
167
|
10 |
|
if ($this->useResettersFromMeta) { |
168
|
|
|
/** @var StateResetter[] $resetters */ |
169
|
7 |
|
$resetters = []; |
170
|
7 |
|
foreach ($this->resetters as $serviceId => $callback) { |
171
|
7 |
|
if (isset($this->instances[$serviceId])) { |
172
|
7 |
|
$resetters[$serviceId] = $callback; |
173
|
|
|
} |
174
|
|
|
} |
175
|
7 |
|
if ($delegatesResetter !== null) { |
176
|
1 |
|
$resetters[] = $delegatesResetter; |
177
|
|
|
} |
178
|
7 |
|
$mainResetter->setResetters($resetters); |
179
|
5 |
|
} elseif ($delegatesResetter !== null) { |
180
|
1 |
|
$resetter = new StateResetter($this->get(ContainerInterface::class)); |
181
|
1 |
|
$resetter->setResetters([$mainResetter, $delegatesResetter]); |
182
|
|
|
|
183
|
1 |
|
return $resetter; |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** @psalm-suppress MixedReturnStatement */ |
188
|
119 |
|
return $this->instances[$id]; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* Sets a definition to the container. Definition may be defined multiple ways. |
193
|
|
|
* |
194
|
|
|
* @param string $id ID to set definition for. |
195
|
|
|
* @param mixed $definition Definition to set. |
196
|
|
|
* |
197
|
|
|
* @throws InvalidConfigException |
198
|
|
|
* |
199
|
|
|
* @see DefinitionNormalizer::normalize() |
200
|
|
|
*/ |
201
|
110 |
|
private function addDefinition(string $id, mixed $definition): void |
202
|
|
|
{ |
203
|
|
|
/** @var mixed $definition */ |
204
|
110 |
|
[$definition, $meta] = DefinitionParser::parse($definition); |
205
|
110 |
|
if ($this->validate) { |
206
|
110 |
|
$this->validateDefinition($definition, $id); |
207
|
107 |
|
$this->validateMeta($meta); |
208
|
|
|
} |
209
|
|
|
/** |
210
|
|
|
* @psalm-var array{reset?:Closure,tags?:string[]} $meta |
211
|
|
|
*/ |
212
|
|
|
|
213
|
101 |
|
if (isset($meta[self::META_TAGS])) { |
214
|
10 |
|
$this->setDefinitionTags($id, $meta[self::META_TAGS]); |
215
|
|
|
} |
216
|
101 |
|
if (isset($meta[self::META_RESET])) { |
217
|
7 |
|
$this->setDefinitionResetter($id, $meta[self::META_RESET]); |
218
|
|
|
} |
219
|
|
|
|
220
|
101 |
|
unset($this->instances[$id]); |
221
|
101 |
|
$this->addDefinitionToStorage($id, $definition); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Sets multiple definitions at once. |
226
|
|
|
* |
227
|
|
|
* @param iterable $config Definitions indexed by their IDs. |
228
|
|
|
* |
229
|
|
|
* @throws InvalidConfigException |
230
|
|
|
*/ |
231
|
135 |
|
private function addDefinitions(iterable $config): void |
232
|
|
|
{ |
233
|
|
|
/** @var mixed $definition */ |
234
|
|
|
/** @psalm-suppress MixedAssignment */ |
235
|
135 |
|
foreach ($config as $id => $definition) { |
236
|
111 |
|
if ($this->validate && !is_string($id)) { |
237
|
1 |
|
throw new InvalidConfigException( |
238
|
1 |
|
sprintf( |
239
|
|
|
'Key must be a string. %s given.', |
240
|
1 |
|
get_debug_type($id) |
241
|
|
|
) |
242
|
|
|
); |
243
|
|
|
} |
244
|
|
|
|
245
|
110 |
|
$id = (string) $id; |
246
|
110 |
|
$this->addDefinition($id, $definition); |
247
|
|
|
} |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Set container delegates. |
252
|
|
|
* |
253
|
|
|
* Each delegate must is a callable in format "function (ContainerInterface $container): ContainerInterface". |
254
|
|
|
* The container instance returned is used in case a service can not be found in primary container. |
255
|
|
|
* |
256
|
|
|
* @param iterable $delegates |
257
|
|
|
* |
258
|
|
|
* @throws InvalidConfigException |
259
|
|
|
*/ |
260
|
119 |
|
private function setDelegates(iterable $delegates): void |
261
|
|
|
{ |
262
|
119 |
|
$this->delegates = new CompositeContainer(); |
263
|
119 |
|
$container = $this->get(ContainerInterface::class); |
264
|
|
|
|
265
|
119 |
|
foreach ($delegates as $delegate) { |
266
|
6 |
|
if (!$delegate instanceof Closure) { |
267
|
1 |
|
throw new InvalidConfigException( |
268
|
|
|
'Delegate must be callable in format "function (ContainerInterface $container): ContainerInterface".' |
269
|
|
|
); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** @var ContainerInterface */ |
273
|
5 |
|
$delegate = $delegate($container); |
274
|
|
|
|
275
|
5 |
|
if (!$delegate instanceof ContainerInterface) { |
276
|
1 |
|
throw new InvalidConfigException( |
277
|
|
|
'Delegate callable must return an object that implements ContainerInterface.' |
278
|
|
|
); |
279
|
|
|
} |
280
|
|
|
|
281
|
4 |
|
$this->delegates->attach($delegate); |
282
|
|
|
} |
283
|
117 |
|
$this->definitions->setDelegateContainer($this->delegates); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* @param mixed $definition Definition to validate. |
288
|
|
|
* @param string|null $id ID of the definition to validate. |
289
|
|
|
* |
290
|
|
|
* @throws InvalidConfigException |
291
|
|
|
*/ |
292
|
110 |
|
private function validateDefinition(mixed $definition, ?string $id = null): void |
293
|
|
|
{ |
294
|
110 |
|
if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) { |
295
|
|
|
/** @var mixed $class */ |
296
|
48 |
|
$class = $definition['class']; |
297
|
|
|
|
298
|
|
|
/** @var mixed $constructorArguments */ |
299
|
48 |
|
$constructorArguments = $definition['__construct()']; |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* @var array $methodsAndProperties Is always array for prepared array definition data. |
303
|
|
|
* |
304
|
|
|
* @see DefinitionParser::parse() |
305
|
|
|
*/ |
306
|
48 |
|
$methodsAndProperties = $definition['methodsAndProperties']; |
307
|
|
|
|
308
|
48 |
|
$definition = array_merge( |
309
|
48 |
|
$class === null ? [] : [ArrayDefinition::CLASS_NAME => $class], |
310
|
|
|
[ArrayDefinition::CONSTRUCTOR => $constructorArguments], |
311
|
|
|
// extract only value from parsed definition method |
312
|
48 |
|
array_map(fn (array $data): mixed => $data[2], $methodsAndProperties), |
313
|
|
|
); |
314
|
|
|
} |
315
|
|
|
|
316
|
110 |
|
if ($definition instanceof ExtensibleService) { |
317
|
1 |
|
throw new InvalidConfigException( |
318
|
|
|
'Invalid definition. ExtensibleService is only allowed in provider extensions.' |
319
|
|
|
); |
320
|
|
|
} |
321
|
|
|
|
322
|
109 |
|
DefinitionValidator::validate($definition, $id); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* @throws InvalidConfigException |
327
|
|
|
*/ |
328
|
107 |
|
private function validateMeta(iterable $meta): void |
329
|
|
|
{ |
330
|
|
|
/** @var mixed $value */ |
331
|
|
|
/** @psalm-suppress MixedAssignment */ |
332
|
107 |
|
foreach ($meta as $key => $value) { |
333
|
23 |
|
$key = (string)$key; |
334
|
23 |
|
if (!in_array($key, self::ALLOWED_META, true)) { |
335
|
3 |
|
throw new InvalidConfigException( |
336
|
3 |
|
sprintf( |
337
|
|
|
'Invalid definition: metadata "%s" is not allowed. Did you mean "%s()" or "$%s"?', |
338
|
|
|
$key, |
339
|
|
|
$key, |
340
|
|
|
$key, |
341
|
|
|
) |
342
|
|
|
); |
343
|
|
|
} |
344
|
|
|
|
345
|
21 |
|
if ($key === self::META_TAGS) { |
346
|
13 |
|
$this->validateDefinitionTags($value); |
347
|
|
|
} |
348
|
|
|
|
349
|
19 |
|
if ($key === self::META_RESET) { |
350
|
8 |
|
$this->validateDefinitionReset($value); |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* @throws InvalidConfigException |
357
|
|
|
*/ |
358
|
13 |
|
private function validateDefinitionTags(mixed $tags): void |
359
|
|
|
{ |
360
|
13 |
|
if (!is_iterable($tags)) { |
361
|
1 |
|
throw new InvalidConfigException( |
362
|
1 |
|
sprintf( |
363
|
|
|
'Invalid definition: tags should be either iterable or array of strings, %s given.', |
364
|
1 |
|
get_debug_type($tags) |
365
|
|
|
) |
366
|
|
|
); |
367
|
|
|
} |
368
|
|
|
|
369
|
12 |
|
foreach ($tags as $tag) { |
370
|
12 |
|
if (!is_string($tag)) { |
371
|
1 |
|
throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.'); |
372
|
|
|
} |
373
|
|
|
} |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* @throws InvalidConfigException |
378
|
|
|
*/ |
379
|
8 |
|
private function validateDefinitionReset(mixed $reset): void |
380
|
|
|
{ |
381
|
8 |
|
if (!$reset instanceof Closure) { |
382
|
1 |
|
throw new InvalidConfigException( |
383
|
1 |
|
sprintf( |
384
|
|
|
'Invalid definition: "reset" should be closure, %s given.', |
385
|
1 |
|
get_debug_type($reset) |
386
|
|
|
) |
387
|
|
|
); |
388
|
|
|
} |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* @throws InvalidConfigException |
393
|
|
|
*/ |
394
|
138 |
|
private function setTags(iterable $tags): void |
395
|
|
|
{ |
396
|
138 |
|
if ($this->validate) { |
397
|
138 |
|
foreach ($tags as $tag => $services) { |
398
|
6 |
|
if (!is_string($tag)) { |
399
|
1 |
|
throw new InvalidConfigException( |
400
|
1 |
|
sprintf( |
401
|
|
|
'Invalid tags configuration: tag should be string, %s given.', |
402
|
1 |
|
get_debug_type($services) |
403
|
|
|
) |
404
|
|
|
); |
405
|
|
|
} |
406
|
5 |
|
if (!is_iterable($services)) { |
407
|
1 |
|
throw new InvalidConfigException( |
408
|
1 |
|
sprintf( |
409
|
|
|
'Invalid tags configuration: tag should be either iterable or array of service IDs, %s given.', |
410
|
1 |
|
get_debug_type($services) |
411
|
|
|
) |
412
|
|
|
); |
413
|
|
|
} |
414
|
|
|
/** @var mixed $service */ |
415
|
4 |
|
foreach ($services as $service) { |
416
|
4 |
|
if (!is_string($service)) { |
417
|
1 |
|
throw new InvalidConfigException( |
418
|
1 |
|
sprintf( |
419
|
|
|
'Invalid tags configuration: service should be defined as class string, %s given.', |
420
|
1 |
|
get_debug_type($service) |
421
|
|
|
) |
422
|
|
|
); |
423
|
|
|
} |
424
|
|
|
} |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
/** @psalm-var iterable<string, iterable<string>> $tags */ |
428
|
|
|
|
429
|
135 |
|
$this->tags = $tags instanceof Traversable ? iterator_to_array($tags, true) : $tags ; |
|
|
|
|
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* @psalm-param string[] $tags |
434
|
|
|
*/ |
435
|
10 |
|
private function setDefinitionTags(string $id, iterable $tags): void |
436
|
|
|
{ |
437
|
10 |
|
foreach ($tags as $tag) { |
438
|
10 |
|
if (!isset($this->tags[$tag])) { |
439
|
8 |
|
$this->tags[$tag] = [$id]; |
440
|
8 |
|
continue; |
441
|
|
|
} |
442
|
|
|
|
443
|
8 |
|
$tags = $this->tags[$tag]; |
444
|
8 |
|
$tags = $tags instanceof Traversable ? iterator_to_array($tags, true) : $tags; |
445
|
8 |
|
if (!in_array($id, $tags, true)) { |
446
|
|
|
/** @psalm-suppress PossiblyInvalidArrayAssignment */ |
447
|
8 |
|
$this->tags[$tag][] = $id; |
448
|
|
|
} |
449
|
|
|
} |
450
|
|
|
} |
451
|
|
|
|
452
|
7 |
|
private function setDefinitionResetter(string $id, Closure $resetter): void |
453
|
|
|
{ |
454
|
7 |
|
$this->resetters[$id] = $resetter; |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* Add definition to storage. |
459
|
|
|
* |
460
|
|
|
* @param string $id ID to set definition for. |
461
|
|
|
* @param mixed|object $definition Definition to set. |
462
|
|
|
* |
463
|
|
|
* @see $definitions |
464
|
|
|
*/ |
465
|
101 |
|
private function addDefinitionToStorage(string $id, $definition): void |
466
|
|
|
{ |
467
|
101 |
|
$this->definitions->set($id, $definition); |
468
|
|
|
|
469
|
101 |
|
if ($id === StateResetter::class) { |
470
|
5 |
|
$this->useResettersFromMeta = false; |
471
|
|
|
} |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* Creates new instance by either interface name or alias. |
476
|
|
|
* |
477
|
|
|
* @param string $id The interface or an alias name that was previously registered. |
478
|
|
|
* |
479
|
|
|
* @throws InvalidConfigException |
480
|
|
|
* @throws NotFoundExceptionInterface |
481
|
|
|
* @throws CircularReferenceException |
482
|
|
|
* |
483
|
|
|
* @return mixed|object New built instance of the specified class. |
484
|
|
|
* |
485
|
|
|
* @internal |
486
|
|
|
*/ |
487
|
119 |
|
private function build(string $id) |
488
|
|
|
{ |
489
|
119 |
|
if (TagHelper::isTagAlias($id)) { |
490
|
11 |
|
return $this->getTaggedServices($id); |
491
|
|
|
} |
492
|
|
|
|
493
|
119 |
|
if (isset($this->building[$id])) { |
494
|
119 |
|
if ($id === ContainerInterface::class) { |
495
|
119 |
|
return $this; |
496
|
|
|
} |
497
|
4 |
|
throw new CircularReferenceException( |
498
|
4 |
|
sprintf( |
499
|
|
|
'Circular reference to "%s" detected while building: %s.', |
500
|
|
|
$id, |
501
|
4 |
|
implode(', ', array_keys($this->building)) |
502
|
|
|
) |
503
|
|
|
); |
504
|
|
|
} |
505
|
|
|
|
506
|
119 |
|
$this->building[$id] = 1; |
507
|
|
|
try { |
508
|
|
|
/** @var mixed $object */ |
509
|
119 |
|
$object = $this->buildInternal($id); |
510
|
|
|
} finally { |
511
|
119 |
|
unset($this->building[$id]); |
512
|
|
|
} |
513
|
|
|
|
514
|
119 |
|
return $object; |
515
|
|
|
} |
516
|
|
|
|
517
|
11 |
|
private function getTaggedServices(string $tagAlias): array |
518
|
|
|
{ |
519
|
11 |
|
$tag = TagHelper::extractTagFromAlias($tagAlias); |
520
|
11 |
|
$services = []; |
521
|
11 |
|
if (isset($this->tags[$tag])) { |
522
|
10 |
|
foreach ($this->tags[$tag] as $service) { |
523
|
|
|
/** @var mixed */ |
524
|
10 |
|
$services[] = $this->get($service); |
525
|
|
|
} |
526
|
|
|
} |
527
|
|
|
|
528
|
11 |
|
return $services; |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
/** |
532
|
|
|
* @throws NotFoundExceptionInterface |
533
|
|
|
* @throws InvalidConfigException |
534
|
|
|
* |
535
|
|
|
* @return mixed|object |
536
|
|
|
*/ |
537
|
119 |
|
private function buildInternal(string $id) |
538
|
|
|
{ |
539
|
119 |
|
if ($this->definitions->has($id)) { |
540
|
119 |
|
$definition = DefinitionNormalizer::normalize($this->definitions->get($id), $id); |
541
|
|
|
|
542
|
119 |
|
return $definition->resolve($this->get(ContainerInterface::class)); |
543
|
|
|
} |
544
|
|
|
|
545
|
11 |
|
throw new NotFoundException($id, $this->definitions->getBuildStack()); |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
/** |
549
|
|
|
* @throws CircularReferenceException |
550
|
|
|
* @throws InvalidConfigException |
551
|
|
|
*/ |
552
|
125 |
|
private function addProviders(iterable $providers): void |
553
|
|
|
{ |
554
|
125 |
|
$extensions = []; |
555
|
|
|
/** @var mixed $provider */ |
556
|
125 |
|
foreach ($providers as $provider) { |
557
|
16 |
|
$providerInstance = $this->buildProvider($provider); |
558
|
14 |
|
$extensions[] = $providerInstance->getExtensions(); |
559
|
14 |
|
$this->addDefinitions($providerInstance->getDefinitions()); |
560
|
|
|
} |
561
|
|
|
|
562
|
123 |
|
foreach ($extensions as $providerExtensions) { |
563
|
|
|
/** @var mixed $extension */ |
564
|
14 |
|
foreach ($providerExtensions as $id => $extension) { |
565
|
10 |
|
if (!is_string($id)) { |
566
|
1 |
|
throw new InvalidConfigException( |
567
|
1 |
|
sprintf('Extension key must be a service ID as string, %s given.', $id) |
568
|
|
|
); |
569
|
|
|
} |
570
|
|
|
|
571
|
9 |
|
if ($id === ContainerInterface::class) { |
572
|
1 |
|
throw new InvalidConfigException('ContainerInterface extensions are not allowed.'); |
573
|
|
|
} |
574
|
|
|
|
575
|
8 |
|
if (!$this->definitions->has($id)) { |
576
|
1 |
|
throw new InvalidConfigException("Extended service \"$id\" doesn't exist."); |
577
|
|
|
} |
578
|
|
|
|
579
|
8 |
|
if (!is_callable($extension)) { |
580
|
1 |
|
throw new InvalidConfigException( |
581
|
1 |
|
sprintf( |
582
|
|
|
'Extension of service should be callable, %s given.', |
583
|
1 |
|
get_debug_type($extension) |
584
|
|
|
) |
585
|
|
|
); |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
/** @var mixed $definition */ |
589
|
7 |
|
$definition = $this->definitions->get($id); |
590
|
7 |
|
if (!$definition instanceof ExtensibleService) { |
591
|
7 |
|
$definition = new ExtensibleService($definition, $id); |
592
|
7 |
|
$this->addDefinitionToStorage($id, $definition); |
593
|
|
|
} |
594
|
|
|
|
595
|
7 |
|
$definition->addExtension($extension); |
596
|
|
|
} |
597
|
|
|
} |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
/** |
601
|
|
|
* Builds service provider by definition. |
602
|
|
|
* |
603
|
|
|
* @param mixed $provider Class name or instance of provider. |
604
|
|
|
* |
605
|
|
|
* @throws InvalidConfigException If provider argument is not valid. |
606
|
|
|
* |
607
|
|
|
* @return ServiceProviderInterface Instance of service provider. |
608
|
|
|
*/ |
609
|
16 |
|
private function buildProvider(mixed $provider): ServiceProviderInterface |
610
|
|
|
{ |
611
|
16 |
|
if ($this->validate && !(is_string($provider) || $provider instanceof ServiceProviderInterface)) { |
612
|
1 |
|
throw new InvalidConfigException( |
613
|
1 |
|
sprintf( |
614
|
|
|
'Service provider should be a class name or an instance of %s. %s given.', |
615
|
|
|
ServiceProviderInterface::class, |
616
|
1 |
|
get_debug_type($provider) |
617
|
|
|
) |
618
|
|
|
); |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
/** |
622
|
|
|
* @psalm-suppress MixedMethodCall Service provider defined as class string |
623
|
|
|
* should container public constructor, otherwise throws error. |
624
|
|
|
*/ |
625
|
15 |
|
$providerInstance = is_object($provider) ? $provider : new $provider(); |
626
|
15 |
|
if (!$providerInstance instanceof ServiceProviderInterface) { |
627
|
1 |
|
throw new InvalidConfigException( |
628
|
1 |
|
sprintf( |
629
|
|
|
'Service provider should be an instance of %s. %s given.', |
630
|
|
|
ServiceProviderInterface::class, |
631
|
1 |
|
get_debug_type($providerInstance) |
632
|
|
|
) |
633
|
|
|
); |
634
|
|
|
} |
635
|
|
|
|
636
|
14 |
|
return $providerInstance; |
637
|
|
|
} |
638
|
|
|
} |
639
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.