256 lines
8.9 KiB
PHP
256 lines
8.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Core\DependencyInjection;
|
|
|
|
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|
use Symfony\Component\DependencyInjection\Definition;
|
|
use Symfony\Component\DependencyInjection\Reference;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
class ServiceProviderCompilationPass implements CompilerPassInterface
|
|
{
|
|
/**
|
|
* @var ServiceProviderRegistry
|
|
*/
|
|
private $registry;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $registryServiceName;
|
|
|
|
public function __construct(ServiceProviderRegistry $registry, string $registryServiceName = 'service_provider_registry')
|
|
{
|
|
$this->registry = $registry;
|
|
$this->registryServiceName = $registryServiceName;
|
|
}
|
|
|
|
/**
|
|
* You can modify the container here before it is dumped to PHP code.
|
|
*
|
|
* @param ContainerBuilder $container
|
|
*/
|
|
public function process(ContainerBuilder $container): void
|
|
{
|
|
// Now, let's store the registry in the container (an empty version of it... it has to be added dynamically at runtime):
|
|
$this->registerRegistry($container);
|
|
|
|
foreach ($this->registry as $serviceProviderKey => $serviceProvider) {
|
|
$this->registerFactories($container, $serviceProviderKey);
|
|
}
|
|
|
|
foreach ($this->registry as $serviceProviderKey => $serviceProvider) {
|
|
$this->registerExtensions($container, $serviceProviderKey);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param ContainerBuilder $container
|
|
*/
|
|
private function registerRegistry(ContainerBuilder $container): void
|
|
{
|
|
$definition = new Definition(ServiceProviderRegistry::class);
|
|
$definition->setSynthetic(true);
|
|
$definition->setPublic(true);
|
|
|
|
$container->setDefinition($this->registryServiceName, $definition);
|
|
}
|
|
|
|
/**
|
|
* @param ContainerBuilder $container
|
|
*/
|
|
private function registerFactories(ContainerBuilder $container, string $serviceProviderKey): void
|
|
{
|
|
$serviceFactories = $this->registry->getFactories($serviceProviderKey);
|
|
|
|
foreach ($serviceFactories as $serviceName => $callable) {
|
|
$this->registerService($container, $serviceName, $serviceProviderKey, $callable);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param ContainerBuilder $container
|
|
*/
|
|
private function registerExtensions(ContainerBuilder $container, string $serviceProviderKey): void
|
|
{
|
|
$serviceExtensions = $this->registry->getExtensions($serviceProviderKey);
|
|
|
|
foreach ($serviceExtensions as $serviceName => $callable) {
|
|
$this->extendService($container, $serviceName, $serviceProviderKey, $callable);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param ContainerBuilder $container
|
|
*/
|
|
private function registerService(
|
|
ContainerBuilder $container,
|
|
string $serviceName,
|
|
string $serviceProviderKey,
|
|
callable $callable
|
|
): void {
|
|
if (!$container->hasDefinition($serviceName)) {
|
|
// Create a new definition
|
|
$factoryDefinition = new Definition();
|
|
$container->setDefinition($serviceName, $factoryDefinition);
|
|
} else {
|
|
// Merge into an existing definition to keep possible addMethodCall/properties configurations
|
|
// (which act like a service extension)
|
|
// Retrieve the existing factory and overwrite it.
|
|
$factoryDefinition = $container->getDefinition($serviceName);
|
|
if ($factoryDefinition->isAutowired()) {
|
|
$factoryDefinition->setAutowired(false);
|
|
}
|
|
}
|
|
|
|
$className = $this->getReturnType($this->getReflection($callable), $serviceName) ?? 'object';
|
|
$factoryDefinition->setClass($className);
|
|
$factoryDefinition->setPublic(true);
|
|
|
|
$staticallyCallable = $this->getStaticallyCallable($callable);
|
|
if ($staticallyCallable !== null) {
|
|
$factoryDefinition->setFactory($staticallyCallable);
|
|
$factoryDefinition->setArguments([
|
|
new Reference('service_container'),
|
|
]);
|
|
} else {
|
|
$factoryDefinition->setFactory([ new Reference($this->registryServiceName), 'createService' ]);
|
|
$factoryDefinition->setArguments([
|
|
$serviceProviderKey,
|
|
$serviceName,
|
|
new Reference('service_container'),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param ContainerBuilder $container
|
|
*/
|
|
private function extendService(ContainerBuilder $container, string $serviceName, string $serviceProviderKey, callable $callable): void
|
|
{
|
|
$finalServiceName = $serviceName;
|
|
$innerName = null;
|
|
|
|
$reflection = $this->getReflection($callable);
|
|
$previousClass = $container->hasDefinition($serviceName) ? $container->getDefinition($serviceName)->getClass() : null;
|
|
$className = $this->getReturnType($reflection, $serviceName) ?? $previousClass ?? 'object';
|
|
|
|
$factoryDefinition = new Definition($className);
|
|
$factoryDefinition->setClass($className);
|
|
$factoryDefinition->setPublic(true);
|
|
|
|
if ($container->has($serviceName)) {
|
|
[$finalServiceName, $previousServiceName] = $this->getDecoratedServiceName($container, $serviceName);
|
|
$innerName = $finalServiceName . '.inner';
|
|
|
|
$factoryDefinition->setDecoratedService($previousServiceName, $innerName);
|
|
} elseif ($reflection->getNumberOfRequiredParameters() > 1) {
|
|
throw new \Exception('A registered extension for the service "' . $serviceName . '" requires the service to be available, which is missing.', 1550092654);
|
|
}
|
|
|
|
$staticallyCallable = $this->getStaticallyCallable($callable);
|
|
if ($staticallyCallable !== null) {
|
|
$factoryDefinition->setFactory($staticallyCallable);
|
|
$factoryDefinition->setArguments([
|
|
new Reference('service_container'),
|
|
]);
|
|
} else {
|
|
$factoryDefinition->setFactory([ new Reference($this->registryServiceName), 'extendService' ]);
|
|
$factoryDefinition->setArguments([
|
|
$serviceProviderKey,
|
|
$serviceName,
|
|
new Reference('service_container'),
|
|
]);
|
|
}
|
|
|
|
if ($innerName !== null) {
|
|
$factoryDefinition->addArgument(new Reference($innerName));
|
|
}
|
|
|
|
$container->setDefinition($finalServiceName, $factoryDefinition);
|
|
}
|
|
|
|
private function getStaticallyCallable(callable $callable): ?callable
|
|
{
|
|
if (is_string($callable)) {
|
|
return $callable;
|
|
}
|
|
if (is_array($callable) && is_string($callable[0])) {
|
|
return $callable;
|
|
}
|
|
|
|
if ($callable instanceof \Closure) {
|
|
$reflection = new \ReflectionFunction($callable);
|
|
$reflectionScope = $reflection->getClosureScopeClass();
|
|
$usedVariables = count($reflection->getClosureUsedVariables());
|
|
|
|
if ($reflection->isStatic() && !$reflection->isAnonymous() && $reflectionScope !== null && $usedVariables === 0) {
|
|
return [
|
|
$reflectionScope->getName(),
|
|
$reflection->getName(),
|
|
];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function getReturnType(\ReflectionFunctionAbstract $reflection, string $serviceName): ?string
|
|
{
|
|
if ($reflection->getReturnType() instanceof \ReflectionNamedType) {
|
|
return $reflection->getReturnType()->getName();
|
|
}
|
|
|
|
if (class_exists($serviceName, true) || interface_exists($serviceName, true)) {
|
|
return $serviceName;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function getReflection(callable $callable): \ReflectionFunctionAbstract
|
|
{
|
|
if (is_array($callable) && count($callable) === 2) {
|
|
return new \ReflectionMethod($callable[0], $callable[1]);
|
|
}
|
|
if (is_object($callable) && !$callable instanceof \Closure) {
|
|
return new \ReflectionMethod($callable, '__invoke');
|
|
}
|
|
|
|
return new \ReflectionFunction($callable);
|
|
}
|
|
|
|
/**
|
|
* @param ContainerBuilder $container
|
|
*/
|
|
private function getDecoratedServiceName(ContainerBuilder $container, string $serviceName): array
|
|
{
|
|
$counter = 1;
|
|
while ($container->has($serviceName . '_decorated_' . $counter)) {
|
|
$counter++;
|
|
}
|
|
return [
|
|
$serviceName . '_decorated_' . $counter,
|
|
$counter === 1 ? $serviceName : $serviceName . '_decorated_' . ($counter - 1),
|
|
];
|
|
}
|
|
}
|