* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; use Symfony\Component\Config\Definition\ArrayNode; use Symfony\Component\Config\Definition\ArrayShapeGenerator; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\PrototypedArrayNode; use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\AppReference; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\Routing\Loader\Configurator\RoutesReference; /** * @internal */ class PhpConfigReferenceDumpPass implements CompilerPassInterface { private const REFERENCE_TEMPLATE = <<<'PHP' * } */ PHPDOC; private const WHEN_ENV_ROUTES_TEMPLATE = <<<'PHPDOC' * "when@{ENV}"?: array, PHPDOC; public function __construct( private string $referenceFile, private array $bundlesDefinition, ) { } public function process(ContainerBuilder $container): void { $knownEnvs = $container->hasParameter('.container.known_envs') ? $container->getParameter('.container.known_envs') : [$container->getParameter('kernel.environment')]; $knownEnvs = array_unique($knownEnvs); sort($knownEnvs); $extensionsPerEnv = []; $appTypes = ''; $anyEnvExtensions = []; $registeredExtensions = $container->getExtensions(); foreach ($this->bundlesDefinition as $bundle => $envs) { if (!is_subclass_of($bundle, BundleInterface::class)) { continue; } if (!$extension = (new $bundle())->getContainerExtension()) { continue; } $extensionAlias = $extension->getAlias(); if (isset($registeredExtensions[$extensionAlias])) { $extension = $registeredExtensions[$extensionAlias]; unset($registeredExtensions[$extensionAlias]); } if (!$configuration = $this->getConfiguration($extension, $container)) { continue; } $tree = $configuration->getConfigTreeBuilder()->buildTree(); if ($tree instanceof ArrayNode && !$tree instanceof PrototypedArrayNode && !$tree->getChildren()) { continue; } $anyEnvExtensions[$extensionAlias] = $extension; $type = $this->camelCase($extensionAlias).'Config'; $appTypes .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($tree)); foreach ($knownEnvs as $env) { if ($envs[$env] ?? $envs['all'] ?? false) { $extensionsPerEnv[$env][] = $extension; } else { unset($anyEnvExtensions[$extensionAlias]); } } } foreach ($registeredExtensions as $alias => $extension) { if (!$configuration = $this->getConfiguration($extension, $container)) { continue; } $tree = $configuration->getConfigTreeBuilder()->buildTree(); if ($tree instanceof ArrayNode && !$tree instanceof PrototypedArrayNode && !$tree->getChildren()) { continue; } $anyEnvExtensions[$alias] = $extension; $type = $this->camelCase($alias).'Config'; $appTypes .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($tree)); } krsort($extensionsPerEnv); $r = new \ReflectionClass(AppReference::class); if (false === $i = strpos($phpdoc = $r->getDocComment(), "\n * @psalm-type ConfigType = ")) { throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', AppReference::class)); } $appTypes = substr_replace($phpdoc, $appTypes, $i, 0); if (false === $i = strrpos($phpdoc = $appTypes, "\n * ...getShapeForExtensions($anyEnvExtensions, $container), $i, 0); $i += \strlen($appTypes) - \strlen($phpdoc); foreach ($extensionsPerEnv as $env => $extensions) { $appTypes = substr_replace($appTypes, strtr(self::WHEN_ENV_APP_TEMPLATE, [ '{ENV}' => $env, '{SHAPE}' => $this->getShapeForExtensions($extensions, $container, ' '), ]), $i, 0); } $appParam = $r->getMethod('config')->getDocComment(); $r = new \ReflectionClass(RoutesReference::class); if (false === $i = strpos($phpdoc = $r->getDocComment(), "\n * @psalm-type RoutesConfig = ")) { throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', RoutesReference::class)); } $routesTypes = ''; foreach ($knownEnvs as $env) { $routesTypes .= strtr(self::WHEN_ENV_ROUTES_TEMPLATE, ['{ENV}' => $env]); } if ('' !== $routesTypes) { $routesTypes = strtr(self::ROUTES_TYPES_TEMPLATE, ['{SHAPE}' => $routesTypes]); $routesTypes = substr_replace($phpdoc, $routesTypes, $i); } $appTypes = str_replace('\\'.ParamConfigurator::class, 'Param', $appTypes); if (!class_exists(Expression::class)) { $appTypes = str_replace('|ExpressionConfigurator', '', $appTypes); } $configReference = strtr(self::REFERENCE_TEMPLATE, [ '{APP_TYPES}' => $appTypes, '{APP_PARAM}' => $appParam, '{ROUTES_TYPES}' => $routesTypes, '{ROUTES_PARAM}' => $r->getMethod('config')->getDocComment(), ]); $dir = \dirname($this->referenceFile); if (is_dir($dir) && is_writable($dir)) { if (!is_file($this->referenceFile) || file_get_contents($this->referenceFile) !== $configReference) { file_put_contents($this->referenceFile, $configReference); } $container->addResource(new FileResource($this->referenceFile)); } } private function camelCase(string $input): string { $output = ucfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); return preg_replace('#\W#', '', $output); } private function getConfiguration(ExtensionInterface $extension, ContainerBuilder $container): ?ConfigurationInterface { return match (true) { $extension instanceof ConfigurationInterface => $extension, $extension instanceof ConfigurationExtensionInterface => $extension->getConfiguration([], $container), default => null, }; } private function getShapeForExtensions(array $extensions, ContainerBuilder $container, string $indent = ''): string { $shape = ''; foreach ($extensions as $extension) { if ($this->getConfiguration($extension, $container)) { $type = $this->camelCase($extension->getAlias()).'Config'; $shape .= \sprintf("\n * %s%s?: %s,", $indent, $extension->getAlias(), $type); } } return $shape; } }