1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18namespace TYPO3\CMS\Core\DependencyInjection;
19
20use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
21use Symfony\Component\DependencyInjection\ContainerBuilder;
22use Symfony\Component\DependencyInjection\Definition;
23use Symfony\Component\DependencyInjection\Reference;
24
25/**
26 * @internal
27 */
28class ServiceProviderCompilationPass implements CompilerPassInterface
29{
30    /**
31     * @var ServiceProviderRegistry
32     */
33    private $registry;
34
35    /**
36     * @var string
37     */
38    private $registryServiceName;
39
40    /**
41     * @param ServiceProviderRegistry $registry
42     * @param string $registryServiceName
43     */
44    public function __construct(ServiceProviderRegistry $registry, string $registryServiceName = 'service_provider_registry')
45    {
46        $this->registry = $registry;
47        $this->registryServiceName = $registryServiceName;
48    }
49
50    /**
51     * You can modify the container here before it is dumped to PHP code.
52     *
53     * @param ContainerBuilder $container
54     */
55    public function process(ContainerBuilder $container): void
56    {
57        // Now, let's store the registry in the container (an empty version of it... it has to be added dynamically at runtime):
58        $this->registerRegistry($container);
59
60        foreach ($this->registry as $serviceProviderKey => $serviceProvider) {
61            $this->registerFactories($container, $serviceProviderKey);
62        }
63
64        foreach ($this->registry as $serviceProviderKey => $serviceProvider) {
65            $this->registerExtensions($container, $serviceProviderKey);
66        }
67    }
68
69    /**
70     * @param ContainerBuilder $container
71     */
72    private function registerRegistry(ContainerBuilder $container): void
73    {
74        $definition = new Definition(ServiceProviderRegistry::class);
75        $definition->setSynthetic(true);
76        $definition->setPublic(true);
77
78        $container->setDefinition($this->registryServiceName, $definition);
79    }
80
81    /**
82     * @param ContainerBuilder $container
83     * @param string $serviceProviderKey
84     */
85    private function registerFactories(ContainerBuilder $container, string $serviceProviderKey): void
86    {
87        $serviceFactories = $this->registry->getFactories($serviceProviderKey);
88
89        foreach ($serviceFactories as $serviceName => $callable) {
90            $this->registerService($container, $serviceName, $serviceProviderKey, $callable);
91        }
92    }
93
94    /**
95     * @param ContainerBuilder $container
96     * @param string $serviceProviderKey
97     */
98    private function registerExtensions(ContainerBuilder $container, string $serviceProviderKey): void
99    {
100        $serviceExtensions = $this->registry->getExtensions($serviceProviderKey);
101
102        foreach ($serviceExtensions as $serviceName => $callable) {
103            $this->extendService($container, $serviceName, $serviceProviderKey, $callable);
104        }
105    }
106
107    /**
108     * @param ContainerBuilder $container
109     * @param string $serviceName
110     * @param string $serviceProviderKey
111     * @param callable $callable
112     */
113    private function registerService(
114        ContainerBuilder $container,
115        string $serviceName,
116        string $serviceProviderKey,
117        callable $callable
118    ): void {
119        if (!$container->has($serviceName)) {
120            // Create a new definition
121            $factoryDefinition = new Definition();
122            $container->setDefinition($serviceName, $factoryDefinition);
123        } else {
124            // Merge into an existing definition to keep possible addMethodCall/properties configurations
125            // (which act like a service extension)
126            // Retrieve the existing factory and overwrite it.
127            $factoryDefinition = $container->findDefinition($serviceName);
128            if ($factoryDefinition->isAutowired()) {
129                $factoryDefinition->setAutowired(false);
130            }
131        }
132
133        $className = $this->getReturnType($this->getReflection($callable), $serviceName) ?? 'object';
134        $factoryDefinition->setClass($className);
135        $factoryDefinition->setPublic(true);
136
137        $staticallyCallable = $this->getStaticallyCallable($callable);
138        if ($staticallyCallable !== null) {
139            $factoryDefinition->setFactory($staticallyCallable);
140            $factoryDefinition->setArguments([
141                new Reference('service_container')
142            ]);
143        } else {
144            $factoryDefinition->setFactory([ new Reference($this->registryServiceName), 'createService' ]);
145            $factoryDefinition->setArguments([
146                $serviceProviderKey,
147                $serviceName,
148                new Reference('service_container')
149            ]);
150        }
151    }
152
153    /**
154     * @param ContainerBuilder $container
155     * @param string $serviceName
156     * @param string $serviceProviderKey
157     * @param callable $callable
158     */
159    private function extendService(ContainerBuilder $container, string $serviceName, string $serviceProviderKey, callable $callable): void
160    {
161        $finalServiceName = $serviceName;
162        $innerName = null;
163
164        $reflection = $this->getReflection($callable);
165        $previousClass = $container->has($serviceName) ? $container->findDefinition($serviceName)->getClass() : null;
166        $className = $this->getReturnType($reflection, $serviceName) ?? $previousClass ?? 'object';
167
168        $factoryDefinition = new Definition($className);
169        $factoryDefinition->setClass($className);
170        $factoryDefinition->setPublic(true);
171
172        if ($container->has($serviceName)) {
173            [$finalServiceName, $previousServiceName] = $this->getDecoratedServiceName($container, $serviceName);
174            $innerName = $finalServiceName . '.inner';
175
176            $factoryDefinition->setDecoratedService($previousServiceName, $innerName);
177        } elseif ($reflection->getNumberOfRequiredParameters() > 1) {
178            throw new \Exception('A registered extension for the service "' . $serviceName . '" requires the service to be available, which is missing.', 1550092654);
179        }
180
181        $staticallyCallable = $this->getStaticallyCallable($callable);
182        if ($staticallyCallable !== null) {
183            $factoryDefinition->setFactory($staticallyCallable);
184            $factoryDefinition->setArguments([
185                new Reference('service_container')
186            ]);
187        } else {
188            $factoryDefinition->setFactory([ new Reference($this->registryServiceName), 'extendService' ]);
189            $factoryDefinition->setArguments([
190                $serviceProviderKey,
191                $serviceName,
192                new Reference('service_container')
193            ]);
194        }
195
196        if ($innerName !== null) {
197            $factoryDefinition->addArgument(new Reference($innerName));
198        }
199
200        $container->setDefinition($finalServiceName, $factoryDefinition);
201    }
202
203    /**
204     * @param callable $callable
205     * @return callable|null
206     */
207    private function getStaticallyCallable(callable $callable): ?callable
208    {
209        if (is_string($callable)) {
210            return $callable;
211        }
212        if (is_array($callable) && isset($callable[0]) && is_string($callable[0])) {
213            return $callable;
214        }
215
216        return null;
217    }
218
219    /**
220     * @param \ReflectionFunctionAbstract $reflection
221     * @param string $serviceName
222     * @return string|null
223     */
224    private function getReturnType(\ReflectionFunctionAbstract $reflection, string $serviceName): ?string
225    {
226        if ($reflection->getReturnType() instanceof \ReflectionNamedType) {
227            return $reflection->getReturnType()->getName();
228        }
229
230        if (class_exists($serviceName, true) || interface_exists($serviceName, true)) {
231            return $serviceName;
232        }
233
234        return null;
235    }
236
237    /**
238     * @param callable $callable
239     * @return \ReflectionFunctionAbstract
240     */
241    private function getReflection(callable $callable): \ReflectionFunctionAbstract
242    {
243        if (is_array($callable) && count($callable) === 2) {
244            return new \ReflectionMethod($callable[0], $callable[1]);
245        }
246        if (is_object($callable) && !$callable instanceof \Closure) {
247            return new \ReflectionMethod($callable, '__invoke');
248        }
249
250        return new \ReflectionFunction($callable);
251    }
252
253    /**
254     * @param ContainerBuilder $container
255     * @param string $serviceName
256     * @return array
257     */
258    private function getDecoratedServiceName(ContainerBuilder $container, string $serviceName): array
259    {
260        $counter = 1;
261        while ($container->has($serviceName . '_decorated_' . $counter)) {
262            $counter++;
263        }
264        return [
265            $serviceName . '_decorated_' . $counter,
266            $counter === 1 ? $serviceName : $serviceName . '_decorated_' . ($counter-1)
267        ];
268    }
269}
270