Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
ContainerBuilder
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
5 / 5
17
100.00% covered (success)
100.00%
1 / 1
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 build
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 resolveInitializer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 adaptInitializer
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 appendConfiguredContainers
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
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 Throwable;
22use FastForward\Config\ConfigInterface;
23use FastForward\Config\Container\ConfigContainer;
24use FastForward\Container\Exception\InvalidArgumentException;
25use Interop\Container\ServiceProviderInterface;
26use Psr\Container\ContainerInterface as PsrContainerInterface;
27
28/**
29 * Registers ordered initializers and builds independent container compositions.
30 *
31 * Registration SHALL retain source objects and class names without resolving services.
32 * Each build() MUST create a fresh aggregate, provider/config adapters, and autowiring
33 * container. Supplied PSR-11 containers SHALL be reused without cloning their state.
34 *
35 * Later registrations SHALL affect subsequent builds only. Provider adapters SHALL
36 * receive their build's aggregate so dependencies can resolve across all sources.
37 */
38 class ContainerBuilder
39{
40    /**
41     * @var list<ConfigInterface|PsrContainerInterface|ServiceProviderInterface|string> the ordered sources retained for subsequent builds
42     */
43    private array $initializers = [];
44
45    /**
46     * Registers one or more initializers for subsequent builds.
47     *
48     * This method SHALL append sources in their supplied order and return this builder.
49     * Class names SHALL be instantiated and adapted when build() is called.
50     * Previously built containers MUST NOT be changed by later registrations.
51     *
52     * @param ConfigInterface|PsrContainerInterface|ServiceProviderInterface|string ...$initializers the configuration objects, containers, providers, or initializer class names
53     *
54     * @return self this builder for incremental, fluent registration
55     */
56    public function add(
57        ConfigInterface|PsrContainerInterface|ServiceProviderInterface|string ...$initializers,
58    ): self {
59        foreach ($initializers as $initializer) {
60            $this->initializers[] = $initializer;
61        }
62
63        return $this;
64    }
65
66    /**
67     * Creates a fresh container stack from the registered initializers.
68     *
69     * This method SHALL adapt sources in registration order, append optional configured
70     * sources, and return an AutowireContainer wrapping the completed aggregate.
71     * Class names MUST be instantiable without required constructor arguments.
72     * The builder SHALL retain registrations without retaining assembled container state.
73     *
74     * @return ContainerInterface the composed and autowire-enabled container
75     *
76     * @throws InvalidArgumentException if an initializer class cannot be found or adapted
77     */
78    public function build(): ContainerInterface
79    {
80        $aggregateContainer = new AggregateContainer();
81
82        foreach ($this->initializers as $initializer) {
83            $container = $this->resolveInitializer($initializer, $aggregateContainer);
84            $aggregateContainer->append($container);
85
86            if ($container instanceof ConfigContainer) {
87                $this->appendConfiguredContainers($container, $aggregateContainer);
88            }
89        }
90
91        return new AutowireContainer($aggregateContainer);
92    }
93
94    /**
95     * Resolves an initializer object or constructs its class before adaptation.
96     *
97     * Existing objects SHALL be passed directly to adapter selection. Class names SHALL
98     * be instantiated with an argument-free constructor for each build.
99     *
100     * @param mixed $initializer an initializer object or instantiable class name
101     * @param AggregateContainer $aggregateContainer the shared resolution wrapper
102     *
103     * @return PsrContainerInterface the adapted initializer
104     *
105     * @throws InvalidArgumentException if an initializer class cannot be found or adapted
106     */
107    private function resolveInitializer(
108        mixed $initializer,
109        AggregateContainer $aggregateContainer
110    ): PsrContainerInterface {
111        return match (true) {
112            \is_object($initializer) => $this->adaptInitializer($initializer, $aggregateContainer),
113            class_exists($initializer) => $this->adaptInitializer(new ($initializer)(), $aggregateContainer),
114            default => throw InvalidArgumentException::forUnsupportedInitializer($initializer),
115        };
116    }
117
118    /**
119     * Selects a container implementation for a constructed initializer.
120     *
121     * PSR-11 containers SHALL take precedence over provider and configuration adapters.
122     * Unsupported objects MUST cause an InvalidArgumentException before reaching the aggregate.
123     *
124     * @param object $initializer the constructed initializer
125     * @param AggregateContainer $aggregateContainer the shared resolution wrapper
126     *
127     * @return PsrContainerInterface the container or provider/config adapter
128     *
129     * @throws InvalidArgumentException if the initializer object cannot be adapted
130     */
131    private function adaptInitializer(
132        object $initializer,
133        AggregateContainer $aggregateContainer
134    ): PsrContainerInterface {
135        return match (true) {
136            $initializer instanceof PsrContainerInterface => $initializer,
137            $initializer instanceof ServiceProviderInterface => new ServiceProviderContainer(
138                $initializer,
139                $aggregateContainer,
140            ),
141            $initializer instanceof ConfigInterface => new ConfigContainer($initializer),
142            default => throw InvalidArgumentException::forUnsupportedInitializer($initializer),
143        };
144    }
145
146    /**
147     * Appends additional initializers declared in a configuration container.
148     *
149     * Entries SHALL be adapted in order using the existing container configuration key.
150     * Failures while reading or adapting this optional configuration SHALL stop that
151     * expansion and be ignored, preserving previously appended and later explicit sources.
152     *
153     * @param ConfigContainer $configContainer the configuration entry source
154     * @param AggregateContainer $aggregateContainer the shared resolution wrapper
155     *
156     * @return void
157     */
158    private function appendConfiguredContainers(
159        ConfigContainer $configContainer,
160        AggregateContainer $aggregateContainer
161    ): void {
162        $configKey = \sprintf('%s.%s', ConfigContainer::ALIAS, ContainerInterface::class);
163
164        try {
165            foreach ($configContainer->get($configKey) as $nested) {
166                $aggregateContainer->append($this->resolveInitializer($nested, $aggregateContainer));
167            }
168        } catch (Throwable) {
169            // Preserve the existing helper's optional configuration behavior.
170        }
171    }
172}