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