Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
202 / 202 |
|
96.08% |
147 / 153 |
|
47.97% |
71 / 148 |
|
83.33% |
20 / 24 |
CRAP | |
0.00% |
0 / 1 |
| Container | |
100.00% |
202 / 202 |
|
96.08% |
147 / 153 |
|
47.97% |
71 / 148 |
|
100.00% |
24 / 24 |
981.30 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
8 / 8 |
|
100.00% |
4 / 4 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| scope | |
100.00% |
8 / 8 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| reset | |
100.00% |
4 / 4 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| has | |
100.00% |
5 / 5 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| entries | |
100.00% |
7 / 7 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
| entry | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get | |
100.00% |
30 / 30 |
|
95.24% |
20 / 21 |
|
66.67% |
8 / 12 |
|
100.00% |
1 / 1 |
12.00 | |||
| definition | |
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| add | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addEntry | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| tag | |
100.00% |
17 / 17 |
|
100.00% |
9 / 9 |
|
57.14% |
4 / 7 |
|
100.00% |
1 / 1 |
6.97 | |||
| new | |
100.00% |
11 / 11 |
|
86.67% |
13 / 15 |
|
21.05% |
4 / 19 |
|
100.00% |
1 / 1 |
23.71 | |||
| resolveEntry | |
100.00% |
13 / 13 |
|
100.00% |
9 / 9 |
|
57.14% |
4 / 7 |
|
100.00% |
1 / 1 |
6.97 | |||
| resolutionContainers | |
100.00% |
5 / 5 |
|
77.78% |
7 / 9 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
4.25 | |||
| materialize | |
100.00% |
35 / 35 |
|
95.24% |
20 / 21 |
|
39.13% |
9 / 23 |
|
100.00% |
1 / 1 |
32.55 | |||
| applyCalls | |
100.00% |
6 / 6 |
|
100.00% |
4 / 4 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
2.15 | |||
| resetScope | |
100.00% |
14 / 14 |
|
100.00% |
12 / 12 |
|
13.89% |
5 / 36 |
|
100.00% |
1 / 1 |
20.96 | |||
| resetIfNeeded | |
100.00% |
7 / 7 |
|
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| trackAndReturn | |
100.00% |
3 / 3 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| findEntry | |
100.00% |
4 / 4 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| root | |
100.00% |
4 / 4 |
|
100.00% |
4 / 4 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| assertMutable | |
100.00% |
2 / 2 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| seal | |
100.00% |
3 / 3 |
|
100.00% |
4 / 4 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
2.15 | |||
| isRoot | |
100.00% |
1 / 1 |
|
100.00% |
5 / 5 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
4.12 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Container; |
| 6 | |
| 7 | use Celemas\Container\Exception\ContainerException; |
| 8 | use Celemas\Container\Exception\NotFoundException; |
| 9 | use Celemas\Wire\CallableResolver; |
| 10 | use Celemas\Wire\Creator; |
| 11 | use Celemas\Wire\Exception\WireException; |
| 12 | use Celemas\Wire\WireContainer; |
| 13 | use Closure; |
| 14 | use Override; |
| 15 | use Psr\Container\ContainerExceptionInterface; |
| 16 | use Psr\Container\ContainerInterface as PsrContainer; |
| 17 | use Throwable; |
| 18 | |
| 19 | /** @psalm-api */ |
| 20 | class Container implements WireContainer |
| 21 | { |
| 22 | protected Creator $creator; |
| 23 | protected readonly ?PsrContainer $wrappedContainer; |
| 24 | protected bool $sealed = false; |
| 25 | |
| 26 | /** @var array<string, Entry> */ |
| 27 | protected array $entries = []; |
| 28 | |
| 29 | /** @var array<string, mixed> */ |
| 30 | protected array $instances = []; |
| 31 | |
| 32 | /** @var array<non-empty-string, self> */ |
| 33 | protected array $tags = []; |
| 34 | |
| 35 | /** @var array<int, Resettable> */ |
| 36 | protected array $usedResettables = []; |
| 37 | |
| 38 | public function __construct( |
| 39 | public readonly bool $autowire = true, |
| 40 | ?PsrContainer $container = null, |
| 41 | protected readonly string $tag = '', |
| 42 | protected readonly ?Container $parent = null, |
| 43 | protected readonly bool $isScope = false, |
| 44 | ) { |
| 45 | if ($container) { |
| 46 | $this->wrappedContainer = $container; |
| 47 | $this->add(PsrContainer::class, $container); |
| 48 | $this->add($container::class, $container); |
| 49 | } else { |
| 50 | $this->wrappedContainer = null; |
| 51 | $this->add(PsrContainer::class, $this); |
| 52 | } |
| 53 | $this->add(Container::class, $this); |
| 54 | $this->creator = new Creator($this); |
| 55 | } |
| 56 | |
| 57 | public function scope(): Container |
| 58 | { |
| 59 | $root = $this->root(); |
| 60 | |
| 61 | if (!$root->sealed) { |
| 62 | $root->seal(); |
| 63 | } |
| 64 | |
| 65 | return new self( |
| 66 | autowire: $root->autowire, |
| 67 | parent: $root, |
| 68 | isScope: true, |
| 69 | ); |
| 70 | } |
| 71 | |
| 72 | public function reset(): void |
| 73 | { |
| 74 | if (!$this->isScope) { |
| 75 | return; |
| 76 | } |
| 77 | |
| 78 | $resetIds = []; |
| 79 | $this->resetScope($resetIds); |
| 80 | } |
| 81 | |
| 82 | #[Override] |
| 83 | public function has(string $id): bool |
| 84 | { |
| 85 | return ( |
| 86 | isset($this->entries[$id]) |
| 87 | || $this->parent?->has($id) |
| 88 | || $this->wrappedContainer?->has($id) |
| 89 | ); |
| 90 | } |
| 91 | |
| 92 | /** @return list<string> */ |
| 93 | public function entries(bool $includeContainer = false): array |
| 94 | { |
| 95 | $keys = array_keys($this->entries); |
| 96 | |
| 97 | if ($includeContainer) { |
| 98 | return $keys; |
| 99 | } |
| 100 | |
| 101 | return array_values(array_filter( |
| 102 | $keys, |
| 103 | static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class), |
| 104 | )); |
| 105 | } |
| 106 | |
| 107 | public function entry(string $id): Entry |
| 108 | { |
| 109 | return $this->entries[$id]; |
| 110 | } |
| 111 | |
| 112 | #[Override] |
| 113 | public function get(string $id): mixed |
| 114 | { |
| 115 | try { |
| 116 | if (array_key_exists($id, $this->instances)) { |
| 117 | return $this->trackAndReturn($this->instances[$id]); |
| 118 | } |
| 119 | |
| 120 | $resolved = $this->findEntry($id); |
| 121 | |
| 122 | if ($resolved !== null) { |
| 123 | return $this->trackAndReturn( |
| 124 | $this->resolveEntry( |
| 125 | entryOwner: $resolved[0], |
| 126 | entry: $resolved[1], |
| 127 | id: $id, |
| 128 | requester: $this, |
| 129 | ), |
| 130 | ); |
| 131 | } |
| 132 | |
| 133 | $wrappedContainer = $this->root()->wrappedContainer; |
| 134 | |
| 135 | if ($wrappedContainer?->has($id)) { |
| 136 | return $this->trackAndReturn($wrappedContainer->get($id)); |
| 137 | } |
| 138 | |
| 139 | // Autowiring: $id does not exists as an entry in the container |
| 140 | if ($this->autowire && class_exists($id)) { |
| 141 | return $this->trackAndReturn($this->creator->create($id)); |
| 142 | } |
| 143 | } catch (WireException $e) { |
| 144 | throw new NotFoundException( |
| 145 | 'Unresolvable id: ' . $id . ' - Details: ' . $e->getMessage(), |
| 146 | previous: $e, |
| 147 | ); |
| 148 | } catch (ContainerExceptionInterface $e) { |
| 149 | throw $e; |
| 150 | } catch (Throwable $e) { |
| 151 | throw new ContainerException( |
| 152 | 'Unresolvable id: ' . $id . ' - ' . $e->getMessage(), |
| 153 | previous: $e, |
| 154 | ); |
| 155 | } |
| 156 | |
| 157 | throw new NotFoundException('Unresolvable id: ' . $id); |
| 158 | } |
| 159 | |
| 160 | #[Override] |
| 161 | public function definition(string $id): mixed |
| 162 | { |
| 163 | $resolved = $this->findEntry($id); |
| 164 | $entry = $resolved[1] ?? null; |
| 165 | |
| 166 | if ($entry !== null) { |
| 167 | return $entry->definition(); |
| 168 | } |
| 169 | |
| 170 | throw new NotFoundException('Unresolvable definition - id: ' . $id); |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * @param non-empty-string $id |
| 175 | */ |
| 176 | public function add( |
| 177 | string $id, |
| 178 | mixed $value = null, |
| 179 | ): Entry { |
| 180 | $this->assertMutable(); |
| 181 | $entry = new Entry($id, $value ?? $id); |
| 182 | $this->entries[$id] = $entry; |
| 183 | unset($this->instances[$id]); |
| 184 | |
| 185 | return $entry; |
| 186 | } |
| 187 | |
| 188 | public function addEntry( |
| 189 | Entry $entry, |
| 190 | ): Entry { |
| 191 | $this->assertMutable(); |
| 192 | $this->entries[$entry->id] = $entry; |
| 193 | unset($this->instances[$entry->id]); |
| 194 | |
| 195 | return $entry; |
| 196 | } |
| 197 | |
| 198 | /** @param non-empty-string $tag */ |
| 199 | public function tag(string $tag): Container |
| 200 | { |
| 201 | if (isset($this->tags[$tag])) { |
| 202 | return $this->tags[$tag]; |
| 203 | } |
| 204 | |
| 205 | if ($this->isRoot() && $this->sealed) { |
| 206 | throw new ContainerException('The root container is sealed after scope() was called'); |
| 207 | } |
| 208 | |
| 209 | $parent = $this; |
| 210 | $isScope = false; |
| 211 | |
| 212 | if ($this->isScope) { |
| 213 | $root = $this->root(); |
| 214 | $parent = $root->tags[$tag] ?? $root; |
| 215 | $isScope = true; |
| 216 | } |
| 217 | |
| 218 | $this->tags[$tag] = new self( |
| 219 | autowire: $this->autowire, |
| 220 | tag: $tag, |
| 221 | parent: $parent, |
| 222 | isScope: $isScope, |
| 223 | ); |
| 224 | |
| 225 | return $this->tags[$tag]; |
| 226 | } |
| 227 | |
| 228 | public function new(string $id, mixed ...$args): object |
| 229 | { |
| 230 | $entry = $this->entries[$id] ?? null; |
| 231 | |
| 232 | if ($entry) { |
| 233 | /** @var mixed $value */ |
| 234 | $value = $entry->definition(); |
| 235 | |
| 236 | if (is_string($value)) { |
| 237 | if (interface_exists($value)) { |
| 238 | return $this->new($value, ...$args); |
| 239 | } |
| 240 | |
| 241 | if (class_exists($value)) { |
| 242 | /** @psalm-suppress MixedMethodCall */ |
| 243 | return new $value(...$args); |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | if (class_exists($id)) { |
| 249 | /** @psalm-suppress MixedMethodCall */ |
| 250 | return new $id(...$args); |
| 251 | } |
| 252 | |
| 253 | throw new NotFoundException('Cannot instantiate ' . $id); |
| 254 | } |
| 255 | |
| 256 | protected function resolveEntry( |
| 257 | Container $entryOwner, |
| 258 | Entry $entry, |
| 259 | string $id, |
| 260 | Container $requester, |
| 261 | ): mixed { |
| 262 | if ($entry->shouldReturnValue()) { |
| 263 | return $entry->definition(); |
| 264 | } |
| 265 | |
| 266 | [$cacheContainer, $resolutionContext] = $this->resolutionContainers( |
| 267 | $entry, |
| 268 | $entryOwner, |
| 269 | $requester, |
| 270 | ); |
| 271 | |
| 272 | if ($cacheContainer !== null && array_key_exists($id, $cacheContainer->instances)) { |
| 273 | return $cacheContainer->instances[$id]; |
| 274 | } |
| 275 | |
| 276 | // materialize() intentionally returns mixed because container entries may resolve to any value type. |
| 277 | /** @psalm-suppress MixedAssignment */ |
| 278 | $result = $this->materialize($entry, $resolutionContext); |
| 279 | |
| 280 | if ($cacheContainer !== null) { |
| 281 | $cacheContainer->instances[$id] = $result; |
| 282 | } |
| 283 | |
| 284 | return $result; |
| 285 | } |
| 286 | |
| 287 | /** |
| 288 | * @return array{0: null|Container, 1: Container} |
| 289 | */ |
| 290 | protected function resolutionContainers( |
| 291 | Entry $entry, |
| 292 | Container $entryOwner, |
| 293 | Container $requester, |
| 294 | ): array { |
| 295 | return match ($entry->getLifetime()) { |
| 296 | Lifetime::Shared => [$entryOwner, $entryOwner], |
| 297 | Lifetime::Scoped => [$requester, $requester], |
| 298 | Lifetime::Transient => [null, $requester], |
| 299 | }; |
| 300 | } |
| 301 | |
| 302 | protected function materialize(Entry $entry, Container $context): mixed |
| 303 | { |
| 304 | /** @var mixed $value */ |
| 305 | $value = $entry->definition(); |
| 306 | |
| 307 | if (is_string($value)) { |
| 308 | if (class_exists($value)) { |
| 309 | $constructor = $entry->getConstructor(); |
| 310 | $args = $entry->getArgs(); |
| 311 | |
| 312 | if (isset($args)) { |
| 313 | // Don't autowire if $args are given |
| 314 | if ($args instanceof Closure) { |
| 315 | $args = $args(...new CallableResolver($context->creator)->resolve($args)); |
| 316 | |
| 317 | return $this->applyCalls($entry, $context->creator->create($value, $args), $context); |
| 318 | } |
| 319 | |
| 320 | return $this->applyCalls( |
| 321 | $entry, |
| 322 | $context->creator->create( |
| 323 | $value, |
| 324 | predefinedArgs: $args, |
| 325 | constructor: $constructor ?? '', |
| 326 | ), |
| 327 | $context, |
| 328 | ); |
| 329 | } |
| 330 | |
| 331 | return $this->applyCalls( |
| 332 | $entry, |
| 333 | $context->creator->create($value, constructor: $constructor ?? ''), |
| 334 | $context, |
| 335 | ); |
| 336 | } |
| 337 | |
| 338 | if ($context->has($value)) { |
| 339 | return $context->get($value); |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | if ($value instanceof Closure) { |
| 344 | $args = $entry->getArgs(); |
| 345 | |
| 346 | if (is_null($args)) { |
| 347 | $args = new CallableResolver($context->creator)->resolve($value); |
| 348 | } elseif ($args instanceof Closure) { |
| 349 | $args = $args(); |
| 350 | } |
| 351 | |
| 352 | return $this->applyCalls($entry, $value(...$args), $context); |
| 353 | } |
| 354 | |
| 355 | if (is_object($value)) { |
| 356 | return $value; |
| 357 | } |
| 358 | |
| 359 | throw new NotFoundException('Unresolvable id: ' . (string) $value); |
| 360 | } |
| 361 | |
| 362 | protected function applyCalls(Entry $entry, mixed $value, Container $context): mixed |
| 363 | { |
| 364 | foreach ($entry->getCalls() as $call) { |
| 365 | $methodToResolve = $call->method; |
| 366 | |
| 367 | /** @var callable */ |
| 368 | $callable = [$value, $methodToResolve]; |
| 369 | $args = new CallableResolver($context->creator)->resolve($callable, $call->args); |
| 370 | $callable(...$args); |
| 371 | } |
| 372 | |
| 373 | return $value; |
| 374 | } |
| 375 | |
| 376 | /** |
| 377 | * @param array<int, true> $resetIds |
| 378 | */ |
| 379 | protected function resetScope(array &$resetIds): void |
| 380 | { |
| 381 | // $instances stores mixed values by design, so foreach assigns mixed to $instance. |
| 382 | /** @psalm-suppress MixedAssignment */ |
| 383 | foreach ($this->instances as $instance) { |
| 384 | $this->resetIfNeeded($instance, $resetIds); |
| 385 | } |
| 386 | |
| 387 | foreach ($this->usedResettables as $usedResettable) { |
| 388 | $this->resetIfNeeded($usedResettable, $resetIds); |
| 389 | } |
| 390 | |
| 391 | foreach ($this->tags as $tagContainer) { |
| 392 | if (!$tagContainer->isScope) { |
| 393 | continue; |
| 394 | } |
| 395 | |
| 396 | $tagContainer->resetScope($resetIds); |
| 397 | } |
| 398 | |
| 399 | $this->instances = []; |
| 400 | $this->entries = []; |
| 401 | $this->tags = []; |
| 402 | $this->usedResettables = []; |
| 403 | $this->add(PsrContainer::class, $this); |
| 404 | $this->add(Container::class, $this); |
| 405 | } |
| 406 | |
| 407 | /** |
| 408 | * @param array<int, true> $resetIds |
| 409 | */ |
| 410 | protected function resetIfNeeded(mixed $value, array &$resetIds): void |
| 411 | { |
| 412 | if (!$value instanceof Resettable) { |
| 413 | return; |
| 414 | } |
| 415 | |
| 416 | $objectId = spl_object_id($value); |
| 417 | |
| 418 | if (isset($resetIds[$objectId])) { |
| 419 | return; |
| 420 | } |
| 421 | |
| 422 | $resetIds[$objectId] = true; |
| 423 | $value->reset(); |
| 424 | } |
| 425 | |
| 426 | protected function trackAndReturn(mixed $value): mixed |
| 427 | { |
| 428 | if ($this->isScope && $value instanceof Resettable) { |
| 429 | $this->usedResettables[spl_object_id($value)] = $value; |
| 430 | } |
| 431 | |
| 432 | return $value; |
| 433 | } |
| 434 | |
| 435 | /** @return array{Container, Entry}|null */ |
| 436 | protected function findEntry(string $id): ?array |
| 437 | { |
| 438 | $entry = $this->entries[$id] ?? null; |
| 439 | |
| 440 | if ($entry !== null) { |
| 441 | return [$this, $entry]; |
| 442 | } |
| 443 | |
| 444 | return $this->parent?->findEntry($id); |
| 445 | } |
| 446 | |
| 447 | protected function root(): Container |
| 448 | { |
| 449 | $container = $this; |
| 450 | |
| 451 | while ($container->parent !== null) { |
| 452 | $container = $container->parent; |
| 453 | } |
| 454 | |
| 455 | return $container; |
| 456 | } |
| 457 | |
| 458 | protected function assertMutable(): void |
| 459 | { |
| 460 | if ($this->sealed) { |
| 461 | throw new ContainerException('The root container is sealed after scope() was called'); |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | protected function seal(): void |
| 466 | { |
| 467 | $this->sealed = true; |
| 468 | |
| 469 | foreach ($this->tags as $tagContainer) { |
| 470 | $tagContainer->seal(); |
| 471 | } |
| 472 | } |
| 473 | |
| 474 | protected function isRoot(): bool |
| 475 | { |
| 476 | return $this->parent === null && $this->tag === '' && !$this->isScope; |
| 477 | } |
| 478 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 39 | public readonly bool $autowire = true, |
| 40 | ?PsrContainer $container = null, |
| 41 | protected readonly string $tag = '', |
| 42 | protected readonly ?Container $parent = null, |
| 43 | protected readonly bool $isScope = false, |
| 44 | ) { |
| 45 | if ($container) { |
| 45 | if ($container) { |
| 46 | $this->wrappedContainer = $container; |
| 50 | $this->wrappedContainer = null; |
| 51 | $this->add(PsrContainer::class, $this); |
| 52 | } |
| 53 | $this->add(Container::class, $this); |
| 53 | $this->add(Container::class, $this); |
| 54 | $this->creator = new Creator($this); |
| 55 | } |
| 177 | string $id, |
| 178 | mixed $value = null, |
| 179 | ): Entry { |
| 180 | $this->assertMutable(); |
| 181 | $entry = new Entry($id, $value ?? $id); |
| 182 | $this->entries[$id] = $entry; |
| 183 | unset($this->instances[$id]); |
| 184 | |
| 185 | return $entry; |
| 186 | } |
| 189 | Entry $entry, |
| 190 | ): Entry { |
| 191 | $this->assertMutable(); |
| 192 | $this->entries[$entry->id] = $entry; |
| 193 | unset($this->instances[$entry->id]); |
| 194 | |
| 195 | return $entry; |
| 196 | } |
| 362 | protected function applyCalls(Entry $entry, mixed $value, Container $context): mixed |
| 363 | { |
| 364 | foreach ($entry->getCalls() as $call) { |
| 364 | foreach ($entry->getCalls() as $call) { |
| 364 | foreach ($entry->getCalls() as $call) { |
| 365 | $methodToResolve = $call->method; |
| 364 | foreach ($entry->getCalls() as $call) { |
| 365 | $methodToResolve = $call->method; |
| 366 | |
| 367 | /** @var callable */ |
| 368 | $callable = [$value, $methodToResolve]; |
| 369 | $args = new CallableResolver($context->creator)->resolve($callable, $call->args); |
| 370 | $callable(...$args); |
| 371 | } |
| 372 | |
| 373 | return $value; |
| 374 | } |
| 460 | if ($this->sealed) { |
| 461 | throw new ContainerException('The root container is sealed after scope() was called'); |
| 463 | } |
| 161 | public function definition(string $id): mixed |
| 162 | { |
| 163 | $resolved = $this->findEntry($id); |
| 164 | $entry = $resolved[1] ?? null; |
| 165 | |
| 166 | if ($entry !== null) { |
| 167 | return $entry->definition(); |
| 170 | throw new NotFoundException('Unresolvable definition - id: ' . $id); |
| 171 | } |
| 93 | public function entries(bool $includeContainer = false): array |
| 94 | { |
| 95 | $keys = array_keys($this->entries); |
| 96 | |
| 97 | if ($includeContainer) { |
| 98 | return $keys; |
| 101 | return array_values(array_filter( |
| 102 | $keys, |
| 103 | static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class), |
| 104 | )); |
| 105 | } |
| 107 | public function entry(string $id): Entry |
| 108 | { |
| 109 | return $this->entries[$id]; |
| 110 | } |
| 436 | protected function findEntry(string $id): ?array |
| 437 | { |
| 438 | $entry = $this->entries[$id] ?? null; |
| 439 | |
| 440 | if ($entry !== null) { |
| 441 | return [$this, $entry]; |
| 444 | return $this->parent?->findEntry($id); |
| 445 | } |
| 113 | public function get(string $id): mixed |
| 114 | { |
| 115 | try { |
| 116 | if (array_key_exists($id, $this->instances)) { |
| 117 | return $this->trackAndReturn($this->instances[$id]); |
| 120 | $resolved = $this->findEntry($id); |
| 121 | |
| 122 | if ($resolved !== null) { |
| 123 | return $this->trackAndReturn( |
| 124 | $this->resolveEntry( |
| 125 | entryOwner: $resolved[0], |
| 133 | $wrappedContainer = $this->root()->wrappedContainer; |
| 134 | |
| 135 | if ($wrappedContainer?->has($id)) { |
| 136 | return $this->trackAndReturn($wrappedContainer->get($id)); |
| 140 | if ($this->autowire && class_exists($id)) { |
| 140 | if ($this->autowire && class_exists($id)) { |
| 140 | if ($this->autowire && class_exists($id)) { |
| 140 | if ($this->autowire && class_exists($id)) { |
| 140 | if ($this->autowire && class_exists($id)) { |
| 140 | if ($this->autowire && class_exists($id)) { |
| 141 | return $this->trackAndReturn($this->creator->create($id)); |
| 141 | return $this->trackAndReturn($this->creator->create($id)); |
| 143 | } catch (WireException $e) { |
| 144 | throw new NotFoundException( |
| 145 | 'Unresolvable id: ' . $id . ' - Details: ' . $e->getMessage(), |
| 148 | } catch (ContainerExceptionInterface $e) { |
| 149 | throw $e; |
| 150 | } catch (Throwable $e) { |
| 151 | throw new ContainerException( |
| 152 | 'Unresolvable id: ' . $id . ' - ' . $e->getMessage(), |
| 157 | throw new NotFoundException('Unresolvable id: ' . $id); |
| 158 | } |
| 83 | public function has(string $id): bool |
| 84 | { |
| 85 | return ( |
| 86 | isset($this->entries[$id]) |
| 87 | || $this->parent?->has($id) |
| 87 | || $this->parent?->has($id) |
| 88 | || $this->wrappedContainer?->has($id) |
| 88 | || $this->wrappedContainer?->has($id) |
| 89 | ); |
| 90 | } |
| 476 | return $this->parent === null && $this->tag === '' && !$this->isScope; |
| 476 | return $this->parent === null && $this->tag === '' && !$this->isScope; |
| 476 | return $this->parent === null && $this->tag === '' && !$this->isScope; |
| 476 | return $this->parent === null && $this->tag === '' && !$this->isScope; |
| 476 | return $this->parent === null && $this->tag === '' && !$this->isScope; |
| 477 | } |
| 302 | protected function materialize(Entry $entry, Container $context): mixed |
| 303 | { |
| 304 | /** @var mixed $value */ |
| 305 | $value = $entry->definition(); |
| 306 | |
| 307 | if (is_string($value)) { |
| 308 | if (class_exists($value)) { |
| 308 | if (class_exists($value)) { |
| 308 | if (class_exists($value)) { |
| 308 | if (class_exists($value)) { |
| 309 | $constructor = $entry->getConstructor(); |
| 310 | $args = $entry->getArgs(); |
| 311 | |
| 312 | if (isset($args)) { |
| 314 | if ($args instanceof Closure) { |
| 315 | $args = $args(...new CallableResolver($context->creator)->resolve($args)); |
| 316 | |
| 317 | return $this->applyCalls($entry, $context->creator->create($value, $args), $context); |
| 320 | return $this->applyCalls( |
| 321 | $entry, |
| 331 | return $this->applyCalls( |
| 332 | $entry, |
| 338 | if ($context->has($value)) { |
| 339 | return $context->get($value); |
| 343 | if ($value instanceof Closure) { |
| 344 | $args = $entry->getArgs(); |
| 345 | |
| 346 | if (is_null($args)) { |
| 346 | if (is_null($args)) { |
| 347 | $args = new CallableResolver($context->creator)->resolve($value); |
| 348 | } elseif ($args instanceof Closure) { |
| 349 | $args = $args(); |
| 350 | } |
| 351 | |
| 352 | return $this->applyCalls($entry, $value(...$args), $context); |
| 352 | return $this->applyCalls($entry, $value(...$args), $context); |
| 355 | if (is_object($value)) { |
| 356 | return $value; |
| 359 | throw new NotFoundException('Unresolvable id: ' . (string) $value); |
| 360 | } |
| 228 | public function new(string $id, mixed ...$args): object |
| 229 | { |
| 230 | $entry = $this->entries[$id] ?? null; |
| 231 | |
| 232 | if ($entry) { |
| 234 | $value = $entry->definition(); |
| 235 | |
| 236 | if (is_string($value)) { |
| 237 | if (interface_exists($value)) { |
| 238 | return $this->new($value, ...$args); |
| 241 | if (class_exists($value)) { |
| 241 | if (class_exists($value)) { |
| 241 | if (class_exists($value)) { |
| 241 | if (class_exists($value)) { |
| 243 | return new $value(...$args); |
| 248 | if (class_exists($id)) { |
| 248 | if (class_exists($id)) { |
| 248 | if (class_exists($id)) { |
| 248 | if (class_exists($id)) { |
| 250 | return new $id(...$args); |
| 253 | throw new NotFoundException('Cannot instantiate ' . $id); |
| 254 | } |
| 74 | if (!$this->isScope) { |
| 75 | return; |
| 78 | $resetIds = []; |
| 79 | $this->resetScope($resetIds); |
| 80 | } |
| 410 | protected function resetIfNeeded(mixed $value, array &$resetIds): void |
| 411 | { |
| 412 | if (!$value instanceof Resettable) { |
| 413 | return; |
| 416 | $objectId = spl_object_id($value); |
| 417 | |
| 418 | if (isset($resetIds[$objectId])) { |
| 419 | return; |
| 422 | $resetIds[$objectId] = true; |
| 423 | $value->reset(); |
| 424 | } |
| 379 | protected function resetScope(array &$resetIds): void |
| 380 | { |
| 381 | // $instances stores mixed values by design, so foreach assigns mixed to $instance. |
| 382 | /** @psalm-suppress MixedAssignment */ |
| 383 | foreach ($this->instances as $instance) { |
| 383 | foreach ($this->instances as $instance) { |
| 383 | foreach ($this->instances as $instance) { |
| 384 | $this->resetIfNeeded($instance, $resetIds); |
| 383 | foreach ($this->instances as $instance) { |
| 384 | $this->resetIfNeeded($instance, $resetIds); |
| 385 | } |
| 386 | |
| 387 | foreach ($this->usedResettables as $usedResettable) { |
| 387 | foreach ($this->usedResettables as $usedResettable) { |
| 387 | foreach ($this->usedResettables as $usedResettable) { |
| 388 | $this->resetIfNeeded($usedResettable, $resetIds); |
| 387 | foreach ($this->usedResettables as $usedResettable) { |
| 388 | $this->resetIfNeeded($usedResettable, $resetIds); |
| 389 | } |
| 390 | |
| 391 | foreach ($this->tags as $tagContainer) { |
| 391 | foreach ($this->tags as $tagContainer) { |
| 392 | if (!$tagContainer->isScope) { |
| 393 | continue; |
| 391 | foreach ($this->tags as $tagContainer) { |
| 392 | if (!$tagContainer->isScope) { |
| 393 | continue; |
| 394 | } |
| 395 | |
| 396 | $tagContainer->resetScope($resetIds); |
| 391 | foreach ($this->tags as $tagContainer) { |
| 392 | if (!$tagContainer->isScope) { |
| 393 | continue; |
| 394 | } |
| 395 | |
| 396 | $tagContainer->resetScope($resetIds); |
| 397 | } |
| 398 | |
| 399 | $this->instances = []; |
| 400 | $this->entries = []; |
| 401 | $this->tags = []; |
| 402 | $this->usedResettables = []; |
| 403 | $this->add(PsrContainer::class, $this); |
| 404 | $this->add(Container::class, $this); |
| 405 | } |
| 291 | Entry $entry, |
| 292 | Container $entryOwner, |
| 293 | Container $requester, |
| 294 | ): array { |
| 295 | return match ($entry->getLifetime()) { |
| 296 | Lifetime::Shared => [$entryOwner, $entryOwner], |
| 297 | Lifetime::Scoped => [$requester, $requester], |
| 298 | Lifetime::Transient => [null, $requester], |
| 298 | Lifetime::Transient => [null, $requester], |
| 298 | Lifetime::Transient => [null, $requester], |
| 296 | Lifetime::Shared => [$entryOwner, $entryOwner], |
| 297 | Lifetime::Scoped => [$requester, $requester], |
| 298 | Lifetime::Transient => [null, $requester], |
| 298 | Lifetime::Transient => [null, $requester], |
| 299 | }; |
| 300 | } |
| 257 | Container $entryOwner, |
| 258 | Entry $entry, |
| 259 | string $id, |
| 260 | Container $requester, |
| 261 | ): mixed { |
| 262 | if ($entry->shouldReturnValue()) { |
| 263 | return $entry->definition(); |
| 266 | [$cacheContainer, $resolutionContext] = $this->resolutionContainers( |
| 267 | $entry, |
| 268 | $entryOwner, |
| 269 | $requester, |
| 270 | ); |
| 271 | |
| 272 | if ($cacheContainer !== null && array_key_exists($id, $cacheContainer->instances)) { |
| 272 | if ($cacheContainer !== null && array_key_exists($id, $cacheContainer->instances)) { |
| 272 | if ($cacheContainer !== null && array_key_exists($id, $cacheContainer->instances)) { |
| 273 | return $cacheContainer->instances[$id]; |
| 278 | $result = $this->materialize($entry, $resolutionContext); |
| 279 | |
| 280 | if ($cacheContainer !== null) { |
| 281 | $cacheContainer->instances[$id] = $result; |
| 282 | } |
| 283 | |
| 284 | return $result; |
| 284 | return $result; |
| 285 | } |
| 449 | $container = $this; |
| 450 | |
| 451 | while ($container->parent !== null) { |
| 451 | while ($container->parent !== null) { |
| 452 | $container = $container->parent; |
| 451 | while ($container->parent !== null) { |
| 455 | return $container; |
| 456 | } |
| 59 | $root = $this->root(); |
| 60 | |
| 61 | if (!$root->sealed) { |
| 62 | $root->seal(); |
| 63 | } |
| 64 | |
| 65 | return new self( |
| 65 | return new self( |
| 66 | autowire: $root->autowire, |
| 67 | parent: $root, |
| 68 | isScope: true, |
| 69 | ); |
| 70 | } |
| 467 | $this->sealed = true; |
| 468 | |
| 469 | foreach ($this->tags as $tagContainer) { |
| 469 | foreach ($this->tags as $tagContainer) { |
| 469 | foreach ($this->tags as $tagContainer) { |
| 470 | $tagContainer->seal(); |
| 469 | foreach ($this->tags as $tagContainer) { |
| 470 | $tagContainer->seal(); |
| 471 | } |
| 472 | } |
| 199 | public function tag(string $tag): Container |
| 200 | { |
| 201 | if (isset($this->tags[$tag])) { |
| 202 | return $this->tags[$tag]; |
| 205 | if ($this->isRoot() && $this->sealed) { |
| 205 | if ($this->isRoot() && $this->sealed) { |
| 205 | if ($this->isRoot() && $this->sealed) { |
| 206 | throw new ContainerException('The root container is sealed after scope() was called'); |
| 209 | $parent = $this; |
| 210 | $isScope = false; |
| 211 | |
| 212 | if ($this->isScope) { |
| 213 | $root = $this->root(); |
| 214 | $parent = $root->tags[$tag] ?? $root; |
| 215 | $isScope = true; |
| 216 | } |
| 217 | |
| 218 | $this->tags[$tag] = new self( |
| 218 | $this->tags[$tag] = new self( |
| 219 | autowire: $this->autowire, |
| 220 | tag: $tag, |
| 221 | parent: $parent, |
| 222 | isScope: $isScope, |
| 223 | ); |
| 224 | |
| 225 | return $this->tags[$tag]; |
| 226 | } |
| 426 | protected function trackAndReturn(mixed $value): mixed |
| 427 | { |
| 428 | if ($this->isScope && $value instanceof Resettable) { |
| 428 | if ($this->isScope && $value instanceof Resettable) { |
| 428 | if ($this->isScope && $value instanceof Resettable) { |
| 429 | $this->usedResettables[spl_object_id($value)] = $value; |
| 430 | } |
| 431 | |
| 432 | return $value; |
| 432 | return $value; |
| 433 | } |
| 103 | static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class), |
| 103 | static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class), |
| 103 | static fn($item) => $item !== PsrContainer::class && !is_subclass_of($item, PsrContainer::class), |