Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
31 / 31 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
1 / 1 |
| AggregateContainer | |
100.00% |
31 / 31 |
|
100.00% |
6 / 6 |
16 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| append | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| prepend | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| has | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
| get | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
| isResolvedByContainer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** |
| 6 | * This file is part of php-fast-forward/container. |
| 7 | * |
| 8 | * This source file is subject to the license bundled |
| 9 | * with this source code in the file LICENSE. |
| 10 | * |
| 11 | * @copyright Copyright (c) 2025-2026 Felipe Sayão Lobato Abreu <github@mentordosnerds.com> |
| 12 | * @license https://opensource.org/licenses/MIT MIT License |
| 13 | * |
| 14 | * @see https://github.com/php-fast-forward/container |
| 15 | * @see https://github.com/php-fast-forward |
| 16 | * @see https://datatracker.ietf.org/doc/html/rfc2119 |
| 17 | */ |
| 18 | |
| 19 | namespace FastForward\Container; |
| 20 | |
| 21 | use FastForward\Container\Exception\NotFoundException; |
| 22 | use Psr\Container\ContainerExceptionInterface; |
| 23 | use Psr\Container\ContainerInterface as PsrContainerInterface; |
| 24 | use Psr\Container\NotFoundExceptionInterface; |
| 25 | |
| 26 | /** |
| 27 | * Aggregates multiple PSR-11 containers and delegates resolution requests among them. |
| 28 | * |
| 29 | * This container implementation respects PSR-11 expectations and throws a |
| 30 | * NotFoundException when a requested service cannot be found in any delegated container. |
| 31 | * |
| 32 | * It caches resolved entries to prevent redundant calls to delegated containers. |
| 33 | */ |
| 34 | class AggregateContainer implements ContainerInterface |
| 35 | { |
| 36 | /** |
| 37 | * @var string container alias for reference binding |
| 38 | */ |
| 39 | public const ALIAS = 'container'; |
| 40 | |
| 41 | /** |
| 42 | * @var ContainerInterface[] the array of containers aggregated by this instance |
| 43 | */ |
| 44 | private array $containers = []; |
| 45 | |
| 46 | /** |
| 47 | * @var array<string, mixed> a registry of already resolved service identifiers |
| 48 | */ |
| 49 | private array $resolved = []; |
| 50 | |
| 51 | /** |
| 52 | * Constructs the AggregateContainer with one or more delegated containers. |
| 53 | * |
| 54 | * The constructor SHALL bind itself to common aliases, including the class name |
| 55 | * and the PSR-11 interface, to simplify resolution of the container itself. |
| 56 | * |
| 57 | * @param PsrContainerInterface ...$containers One or more container implementations to aggregate. |
| 58 | */ |
| 59 | public function __construct(PsrContainerInterface ...$containers) |
| 60 | { |
| 61 | $this->resolved = [ |
| 62 | self::ALIAS => $this, |
| 63 | self::class => $this, |
| 64 | ContainerInterface::class => $this, |
| 65 | PsrContainerInterface::class => $this, |
| 66 | ]; |
| 67 | |
| 68 | foreach ($containers as $container) { |
| 69 | $this->append($container); |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | /** |
| 74 | * Appends a container to the end of the aggregated list. |
| 75 | * |
| 76 | * This method MAY be used to dynamically expand the resolution pool. |
| 77 | * |
| 78 | * @param PsrContainerInterface $container the container to append |
| 79 | * |
| 80 | * @return void |
| 81 | */ |
| 82 | public function append(PsrContainerInterface $container): void |
| 83 | { |
| 84 | $this->containers[] = $container; |
| 85 | |
| 86 | if (! isset($this->resolved[$container::class])) { |
| 87 | $this->resolved[$container::class] = $container; |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Prepends a container to the beginning of the aggregated list. |
| 93 | * |
| 94 | * This method MAY be used to prioritize a container during resolution. |
| 95 | * |
| 96 | * @param PsrContainerInterface $container the container to prepend |
| 97 | * |
| 98 | * @return void |
| 99 | */ |
| 100 | public function prepend(PsrContainerInterface $container): void |
| 101 | { |
| 102 | $this->resolved[$container::class] = $container; |
| 103 | array_unshift($this->containers, $container); |
| 104 | } |
| 105 | |
| 106 | /** |
| 107 | * Determines whether a service identifier can be resolved. |
| 108 | * |
| 109 | * This method SHALL return true if the identifier is pre-resolved or can be located |
| 110 | * in any of the aggregated containers. |
| 111 | * |
| 112 | * @param string $id the identifier of the entry to look for |
| 113 | * |
| 114 | * @return bool true if the entry exists, false otherwise |
| 115 | */ |
| 116 | public function has(string $id): bool |
| 117 | { |
| 118 | if ($this->isResolvedByContainer($id)) { |
| 119 | return true; |
| 120 | } |
| 121 | |
| 122 | foreach ($this->containers as $container) { |
| 123 | if ($container->has($id)) { |
| 124 | return true; |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | return false; |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Retrieves the entry associated with the given identifier. |
| 133 | * |
| 134 | * This method SHALL resolve from its internal cache first, and otherwise iterate |
| 135 | * through the aggregated containers to resolve the entry. It MUST throw a |
| 136 | * NotFoundException if the identifier cannot be resolved. |
| 137 | * |
| 138 | * @param string $id the identifier of the entry to retrieve |
| 139 | * |
| 140 | * @return mixed the resolved entry |
| 141 | * |
| 142 | * @throws NotFoundException if the identifier cannot be found in any aggregated container |
| 143 | * @throws ContainerExceptionInterface if the container cannot resolve the entry |
| 144 | */ |
| 145 | public function get(string $id): mixed |
| 146 | { |
| 147 | if ($this->isResolvedByContainer($id)) { |
| 148 | return $this->resolved[$id]; |
| 149 | } |
| 150 | |
| 151 | $exception = NotFoundException::forServiceID($id); |
| 152 | |
| 153 | foreach ($this->containers as $container) { |
| 154 | if (! $container->has($id)) { |
| 155 | continue; |
| 156 | } |
| 157 | |
| 158 | try { |
| 159 | $this->resolved[$id] = $container->get($id); |
| 160 | |
| 161 | return $this->resolved[$id]; |
| 162 | } catch (NotFoundExceptionInterface) { |
| 163 | // Ignore NotFoundExceptionInterface |
| 164 | } catch (ContainerExceptionInterface) { |
| 165 | // Future enhancement: Replace with a domain-specific exception if desired |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | throw $exception; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Determines whether the identifier has already been resolved by this container. |
| 174 | * |
| 175 | * This method is used internally to avoid unnecessary delegation to sub-containers. |
| 176 | * |
| 177 | * @param string $id the identifier to check |
| 178 | * |
| 179 | * @return bool true if the identifier is already resolved, false otherwise |
| 180 | */ |
| 181 | private function isResolvedByContainer(string $id): bool |
| 182 | { |
| 183 | return \array_key_exists($id, $this->resolved); |
| 184 | } |
| 185 | } |