Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
AggregateServiceProvider
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
3 / 3
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFactories
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getExtensions
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
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\ServiceProvider;
20
21use FastForward\Container\Exception\RuntimeException;
22use FastForward\Container\Factory\ServiceFactory;
23use Interop\Container\ServiceProviderInterface;
24use Psr\Container\ContainerInterface;
25
26use function DI\string;
27
28/**
29 * Aggregates multiple service providers into a single provider.
30 *
31 * This class MUST be used to compose a unified list of factories and extensions
32 * from several ServiceProviderInterface implementations.
33 *
34 * Factories and extensions returned by this class are merged in registration order.
35 */
36class AggregateServiceProvider implements ServiceProviderInterface
37{
38    /**
39     * @var ServiceProviderInterface[] list of service providers to aggregate
40     */
41    private  array $serviceProviders;
42
43    /**
44     * Constructs the AggregateServiceProvider.
45     *
46     * @param ServiceProviderInterface ...$serviceProviders One or more service providers to aggregate.
47     */
48    public function __construct(ServiceProviderInterface ...$serviceProviders)
49    {
50        $this->serviceProviders = $serviceProviders;
51    }
52
53    /**
54     * Retrieves all service factories from aggregated providers.
55     *
56     * This method merges the factories from each service provider into a single array.
57     * The factory for this class itself is added under the key of its class name.
58     *
59     * @return array<string, callable> an associative array of service factories
60     */
61    public function getFactories(): array
62    {
63        $serviceProviders = array_reduce(
64            $this->serviceProviders,
65            static fn(array $carry, ServiceProviderInterface $provider): array => $carry + [
66                $provider::class => new ServiceFactory($provider),
67            ],
68            [
69                static::class => new ServiceFactory($this),
70            ],
71        );
72
73        return array_reduce(
74            $this->serviceProviders,
75            static fn($factories, $serviceProvider): array => array_merge($factories, $serviceProvider->getFactories()),
76            $serviceProviders,
77        );
78    }
79
80    /**
81     * Retrieves all service extensions from aggregated providers.
82     *
83     * This method merges extensions from each provider. If multiple extensions exist for
84     * the same service ID, they are composed in the order they are added using nested closures.
85     *
86     * @return array<string, callable> an associative array of service extensions
87     *
88     * @throws \RuntimeException if any extension is not callable
89     */
90    public function getExtensions(): array
91    {
92        return array_reduce(
93            $this->serviceProviders,
94            static function (array $extensions, ServiceProviderInterface $serviceProvider): array {
95                foreach ($serviceProvider->getExtensions() as $id => $extension) {
96                    if (! \is_callable($extension)) {
97                        throw RuntimeException::forNonCallableExtension($id, get_debug_type($extension));
98                    }
99
100                    $extensions[$id] = \array_key_exists($id, $extensions)
101                        ? static fn(ContainerInterface $container, $previous) => $extension(
102                            $container,
103                            $extensions[$id]($container, $previous)
104                        )
105                        : $extension;
106                }
107
108                return $extensions;
109            },
110            []
111        );
112    }
113}