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