Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
AggregateContainer
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
6 / 6
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 append
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 prepend
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 get
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 isResolvedByContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(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
19namespace FastForward\Container;
20
21use FastForward\Container\Exception\NotFoundException;
22use Psr\Container\ContainerExceptionInterface;
23use Psr\Container\ContainerInterface as PsrContainerInterface;
24use 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 */
34class 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}