diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 8e70fb98e42fe..181a4dbcc1ead 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -25,6 +25,7 @@ CHANGELOG * Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default * Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead * Allow configuring compound rate limiters + * Add configuration class and config traits generation 7.2 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php index 48ed51aecb14e..98f50ec24ae16 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php @@ -13,7 +13,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Config\Builder\ConfigBuilderGenerator; -use Symfony\Component\Config\Builder\ConfigBuilderGeneratorInterface; +use Symfony\Component\Config\Builder\ConfigClassAwareBuilderGeneratorInterface; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -29,6 +29,7 @@ * Generate all config builders. * * @author Tobias Nyholm + * @author Alexandre Daubois * * @final since Symfony 7.1 */ @@ -68,19 +69,31 @@ public function warmUp(string $cacheDir, ?string $buildDir = null): array } } + $configurations = []; foreach ($extensions as $extension) { + if (null === $configuration = $this->getConfigurationFromExtension($extension)) { + continue; + } + + $alias = lcfirst(str_replace('_', '', ucwords($extension->getAlias(), '_'))); + $configurations[$alias] = $configuration; + try { - $this->dumpExtension($extension, $generator); + $generator->build($configurations[$alias]); } catch (\Exception $e) { $this->logger?->warning('Failed to generate ConfigBuilder for extension {extensionClass}: '.$e->getMessage(), ['exception' => $e, 'extensionClass' => $extension::class]); } } + if ($generator instanceof ConfigClassAwareBuilderGeneratorInterface && $configurations) { + $generator->buildConfigClassAndTraits($configurations); + } + // No need to preload anything return []; } - private function dumpExtension(ExtensionInterface $extension, ConfigBuilderGeneratorInterface $generator): void + private function getConfigurationFromExtension(ExtensionInterface $extension): ?ConfigurationInterface { $configuration = null; if ($extension instanceof ConfigurationInterface) { @@ -90,11 +103,7 @@ private function dumpExtension(ExtensionInterface $extension, ConfigBuilderGener $configuration = $extension->getConfiguration([], new ContainerBuilder($container instanceof Container ? new ContainerBag($container) : new ParameterBag())); } - if (!$configuration) { - return; - } - - $generator->build($configuration); + return $configuration; } public function isOptional(): bool diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php index 9941518074017..fb212422fcf32 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\CacheWarmer\ConfigBuilderCacheWarmer; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\Config\Builder\ConfigClassAwareBuilderGeneratorInterface; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -182,6 +183,11 @@ public function getCharset(): string $warmer->warmUp($kernel->getCacheDir(), $kernel->getBuildDir()); self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php'); + + if (interface_exists(ConfigClassAwareBuilderGeneratorInterface::class)) { + self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkTrait.php'); + self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Config.php'); + } } public function testExtensionAddedInKernel() @@ -222,6 +228,11 @@ public function getAlias(): string self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php'); self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/AppConfig.php'); + + if (interface_exists(ConfigClassAwareBuilderGeneratorInterface::class)) { + self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkTrait.php'); + self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Config.php'); + } } public function testKernelAsExtension() @@ -267,6 +278,11 @@ public function getConfigTreeBuilder(): TreeBuilder self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php'); self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/KernelConfig.php'); + + if (interface_exists(ConfigClassAwareBuilderGeneratorInterface::class)) { + self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkTrait.php'); + self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Config.php'); + } } public function testExtensionsExtendedInBuildMethods() @@ -333,6 +349,11 @@ public function addConfiguration(NodeDefinition $node): void self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig.php'); self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig/FormLoginConfig.php'); self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig/TokenConfig.php'); + + if (interface_exists(ConfigClassAwareBuilderGeneratorInterface::class)) { + self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkTrait.php'); + self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Config.php'); + } } } diff --git a/src/Symfony/Component/Config/Builder/ArrayShapeGenerator.php b/src/Symfony/Component/Config/Builder/ArrayShapeGenerator.php new file mode 100644 index 0000000000000..4fb6ec56da3a3 --- /dev/null +++ b/src/Symfony/Component/Config/Builder/ArrayShapeGenerator.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +use Symfony\Component\Config\Definition\ArrayNode; +use Symfony\Component\Config\Definition\BaseNode; +use Symfony\Component\Config\Definition\BooleanNode; +use Symfony\Component\Config\Definition\EnumNode; +use Symfony\Component\Config\Definition\FloatNode; +use Symfony\Component\Config\Definition\IntegerNode; +use Symfony\Component\Config\Definition\NodeInterface; +use Symfony\Component\Config\Definition\NumericNode; +use Symfony\Component\Config\Definition\PrototypedArrayNode; +use Symfony\Component\Config\Definition\ScalarNode; +use Symfony\Component\Config\Definition\StringNode; +use Symfony\Component\Config\Definition\VariableNode; + +/** + * @author Alexandre Daubois + * + * @internal + */ +final class ArrayShapeGenerator +{ + public static function generate(ArrayNode $node): string + { + return self::prependPhpDocWithStar(self::doGeneratePhpDoc($node)); + } + + private static function doGeneratePhpDoc(NodeInterface $node, int $nestingLevel = 1): string + { + if (!$node instanceof ArrayNode) { + return $node->getName(); + } + + if ($node instanceof PrototypedArrayNode) { + $isHashmap = (bool) $node->getKeyAttribute(); + + $prototype = $node->getPrototype(); + if ($prototype instanceof ArrayNode) { + return 'array<'.($isHashmap ? 'string, ' : '').self::doGeneratePhpDoc($prototype, $nestingLevel).'>'; + } + + return 'array<'.($isHashmap ? 'string, ' : '').self::handleScalarNode($prototype).'>'; + } + + if (!($children = $node->getChildren()) && !$node->getParent() instanceof PrototypedArrayNode) { + return 'array'; + } + + $arrayShape = \sprintf("array{%s\n", self::generateInlinePhpDocForNode($node)); + + /** @var NodeInterface $child */ + foreach ($children as $child) { + $arrayShape .= str_repeat(' ', $nestingLevel * 4).self::dumpNodeKey($child).': '; + + if ($child instanceof PrototypedArrayNode) { + $isHashmap = (bool) $child->getKeyAttribute(); + + $arrayShape .= 'array<'.($isHashmap ? 'string, ' : '').self::handleNode($child->getPrototype(), $nestingLevel).'>'; + } else { + $arrayShape .= self::handleNode($child, $nestingLevel); + } + + $arrayShape .= \sprintf(",%s\n", !$child instanceof ArrayNode ? self::generateInlinePhpDocForNode($child) : ''); + } + + return $arrayShape.str_repeat(' ', 4 * ($nestingLevel - 1)).'}'; + } + + private static function dumpNodeKey(NodeInterface $node): string + { + $name = $node->getName(); + $quoted = str_starts_with($name, '@') + || \in_array(strtolower($name), ['int', 'float', 'bool', 'null', 'scalar'], true) + || strpbrk($name, '\'"'); + + if ($quoted) { + $name = "'".addslashes($name)."'"; + } + + return $name.($node->isRequired() ? '' : '?'); + } + + private static function handleNumericNode(NumericNode $node): string + { + $min = $node->getMin() ?? 'min'; + $max = $node->getMax() ?? 'max'; + + if ($node instanceof IntegerNode) { + return \sprintf('int<%s, %s>', $min, $max); + } elseif ($node instanceof FloatNode) { + return 'float'; + } + + return \sprintf('int<%s, %s>|float', $min, $max); + } + + private static function prependPhpDocWithStar(string $shape): string + { + return str_replace("\n", "\n * ", $shape); + } + + private static function generateInlinePhpDocForNode(BaseNode $node): string + { + $comment = ''; + if ($node->hasDefaultValue() || $node->getInfo() || $node->isDeprecated()) { + if ($node->isDeprecated()) { + $comment .= 'Deprecated: '.$node->getDeprecation($node->getName(), $node->getPath())['message'].' '; + } + + if ($info = $node->getInfo()) { + $comment .= $info.' '; + } + + if ($node->hasDefaultValue() && !\is_array($defaultValue = $node->getDefaultValue())) { + $comment .= 'Default: '.json_encode($defaultValue, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRESERVE_ZERO_FRACTION); + } + } + + return $comment ? ' // '.rtrim(preg_replace('/\s+/', ' ', $comment)) : ''; + } + + private static function handleNode(NodeInterface $node, int $nestingLevel): string + { + if ($node instanceof ArrayNode) { + return self::doGeneratePhpDoc($node, 1 + $nestingLevel); + } + + return self::handleScalarNode($node); + } + + private static function handleScalarNode(NodeInterface $node): string + { + return match (true) { + $node instanceof BooleanNode => 'bool', + $node instanceof StringNode => 'string', + $node instanceof NumericNode => self::handleNumericNode($node), + $node instanceof EnumNode => $node->getPermissibleValues('|'), + $node instanceof ScalarNode => 'string|int|float|bool', + $node instanceof VariableNode => 'mixed', + }; + } +} diff --git a/src/Symfony/Component/Config/Builder/ClassBuilder.php b/src/Symfony/Component/Config/Builder/ClassBuilder.php index 5ae8bda169a19..aaa846dd77b72 100644 --- a/src/Symfony/Component/Config/Builder/ClassBuilder.php +++ b/src/Symfony/Component/Config/Builder/ClassBuilder.php @@ -31,6 +31,7 @@ class ClassBuilder private array $use = []; private array $implements = []; private bool $allowExtraKeys = false; + private array $traits = []; public function __construct( private string $namespace, @@ -72,6 +73,9 @@ public function build(): string $implements = [] === $this->implements ? '' : 'implements '.implode(', ', $this->implements); $body = ''; + foreach ($this->traits as $trait) { + $body .= ' use '.$trait.";\n"; + } foreach ($this->properties as $property) { $body .= ' '.$property->getContent()."\n"; } @@ -107,6 +111,11 @@ public function addUse(string $class): void $this->use[$class] = true; } + public function addTrait(string $trait): void + { + $this->traits[] = '\\'.ltrim($trait, '\\'); + } + public function addImplements(string $interface): void { $this->implements[] = '\\'.ltrim($interface, '\\'); diff --git a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php index 08e91c2d11105..4256d74b8f669 100644 --- a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php +++ b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Config\Builder; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration; use Symfony\Component\Config\Definition\ArrayNode; use Symfony\Component\Config\Definition\BaseNode; use Symfony\Component\Config\Definition\BooleanNode; @@ -25,13 +26,17 @@ use Symfony\Component\Config\Definition\ScalarNode; use Symfony\Component\Config\Definition\VariableNode; use Symfony\Component\Config\Loader\ParamConfigurator; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Resource\ImportsTrait; +use Symfony\Component\DependencyInjection\Resource\ParametersTrait; +use Symfony\Component\DependencyInjection\Resource\ServicesTrait; /** * Generate ConfigBuilders to help create valid config. * * @author Tobias Nyholm */ -class ConfigBuilderGenerator implements ConfigBuilderGeneratorInterface +class ConfigBuilderGenerator implements ConfigBuilderGeneratorInterface, ConfigClassAwareBuilderGeneratorInterface { /** * @var ClassBuilder[] @@ -55,9 +60,9 @@ public function build(ConfigurationInterface $configuration): \Closure $path = $this->getFullPath($rootClass); if (!is_file($path)) { - // Generate the class if the file not exists - $this->classes[] = $rootClass; + // Generate the class if the file doesn't exist $this->buildNode($rootNode, $rootClass, $this->getSubNamespace($rootClass)); + $this->buildConfigureOption($rootNode, $rootClass); $rootClass->addImplements(ConfigBuilderInterface::class); $rootClass->addMethod('getExtensionAlias', ' public function NAME(): string @@ -65,6 +70,7 @@ public function NAME(): string return \'ALIAS\'; }', ['ALIAS' => $rootNode->getPath()]); + $this->writeRootClass($rootClass); $this->writeClasses(); } @@ -76,6 +82,68 @@ public function NAME(): string }; } + /** + * @param array $configurations + */ + public function buildConfigClassAndTraits(array $configurations): \Closure + { + $traitsClosure = (new ConfigTraitsGenerator($this->outputDir))->build($configurations); + + $class = new ClassBuilder('Symfony\\Config', ''); + $class->addTrait(ImportsTrait::class); + $class->addTrait(ParametersTrait::class); + $class->addTrait(ServicesTrait::class); + $class->addUse(ContainerConfigurator::class); + + $class->addProperty('builders', 'array'); + + foreach ($configurations as $alias => $configuration) { + $class->addUse('Symfony\\Config\\'.ucfirst($this->camelCase($alias)).'Config'); + $class->addTrait('Symfony\\Config\\'.ucfirst($this->camelCase($alias)).'Trait'); + } + + $configurationKeys = array_keys($configurations); + + foreach ($configurationKeys as $configurationKey) { + $camelCaseKey = $this->camelCase($configurationKey); + + $class->addProperty(sprintf('%sConfig', $camelCaseKey), sprintf('%sConfig', ucfirst($camelCaseKey))); + } + + $class->addMethod('__construct', <<<'PHP' + public function __construct(public readonly ?string $env) + { + CONFIGS + $this->builders = [BUILDERS]; + } + PHP, [ + 'BUILDERS' => implode(", ", array_map(fn ($alias) => sprintf('$this->%sConfig', $this->camelCase($alias)), $configurationKeys)), + 'CONFIGS' => implode("\n", array_map(fn ($alias) => sprintf(' $this->%sConfig = new %sConfig();', $this->camelCase($alias), ucfirst($this->camelCase($alias))), $configurationKeys)), + ]); + + $class->addMethod('getBuilders', <<<'PHP' + public function getBuilders(): array + { + return $this->builders; + } + PHP); + + $path = $this->getFullPath($class); + file_put_contents($path, $class->build()); + + return function () use ($path, $traitsClosure) { + $traitsClosure(); + return require_once $path; + }; + } + + private function camelCase(string $input): string + { + $output = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + + return preg_replace('#\W#', '', $output); + } + private function getFullPath(ClassBuilder $class): string { $directory = $this->outputDir.\DIRECTORY_SEPARATOR.$class->getDirectory(); @@ -86,6 +154,18 @@ private function getFullPath(ClassBuilder $class): string return $directory.\DIRECTORY_SEPARATOR.$class->getFilename(); } + private function writeRootClass(ClassBuilder $rootClass): void + { + $this->buildConstructor($rootClass); + $this->buildToArray($rootClass, true); + if ($rootClass->getProperties()) { + $rootClass->addProperty('_usedProperties', null, '[]'); + } + $this->buildSetExtraKey($rootClass); + + file_put_contents($this->getFullPath($rootClass), $rootClass->build()); + } + private function writeClasses(): void { foreach ($this->classes as $class) { @@ -102,6 +182,24 @@ private function writeClasses(): void $this->classes = []; } + private function buildConfigureOption(ArrayNode $node, ClassBuilder $class): void + { + $class->addProperty('configOutput', defaultValue: '[]'); + + $body = ' +public function NAME( + /** @var PHPDOC_ARRAY_SHAPE $config */ + PARAM_TYPE $config = []): void +{ + $this->configOutput = $config; +}'; + + $class->addMethod('configure', $body, [ + 'PARAM_TYPE' => 'array', + 'PHPDOC_ARRAY_SHAPE' => str_replace("\n", "\n ", ArrayShapeGenerator::generate($node)), + ]); + } + private function buildNode(NodeInterface $node, ClassBuilder $class, string $namespace): void { if (!$node instanceof ArrayNode) { @@ -469,10 +567,24 @@ private function getSingularName(PrototypedArrayNode $node): string return $name; } - private function buildToArray(ClassBuilder $class): void + private function buildToArray(ClassBuilder $class, bool $rootClass = false): void { - $body = '$output = [];'; + $body = ''; + if ($rootClass) { + $body = 'if ($this->configOutput) { + return $this->configOutput; + } + + '; + } + + $body .= '$output = [];'; + foreach ($class->getProperties() as $p) { + if ('configOutput' === $p->getName()) { + continue; + } + $code = '$this->PROPERTY'; if (null !== $p->getType()) { if ($p->isArray()) { @@ -509,6 +621,10 @@ private function buildConstructor(ClassBuilder $class): void { $body = ''; foreach ($class->getProperties() as $p) { + if ('configOutput' === $p->getName()) { + continue; + } + $code = '$value[\'ORG_NAME\']'; if (null !== $p->getType()) { if ($p->isArray()) { diff --git a/src/Symfony/Component/Config/Builder/ConfigClassAwareBuilderGeneratorInterface.php b/src/Symfony/Component/Config/Builder/ConfigClassAwareBuilderGeneratorInterface.php new file mode 100644 index 0000000000000..80c3e0bc50f62 --- /dev/null +++ b/src/Symfony/Component/Config/Builder/ConfigClassAwareBuilderGeneratorInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +use Symfony\Component\Config\Definition\ConfigurationInterface; + +/** + * @author Alexandre Daubois + */ +interface ConfigClassAwareBuilderGeneratorInterface +{ + /** + * @param array $configurations Configurations indexed by their alias + */ + public function buildConfigClassAndTraits(array $configurations): \Closure; +} diff --git a/src/Symfony/Component/Config/Builder/ConfigTraitsGenerator.php b/src/Symfony/Component/Config/Builder/ConfigTraitsGenerator.php new file mode 100644 index 0000000000000..8474aab84b69a --- /dev/null +++ b/src/Symfony/Component/Config/Builder/ConfigTraitsGenerator.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +use Symfony\Component\Config\Definition\ConfigurationInterface; + +/** + * @author Alexandre Daubois + * + * @internal + */ +final class ConfigTraitsGenerator +{ + public function __construct( + private string $outputDir, + ) { + } + + /** + * @param array $configurations Configurations indexed by their alias + */ + public function build(array $configurations): \Closure + { + $paths = []; + foreach ($configurations as $alias => $configuration) { + if (trait_exists('\Symfony\Config\\'.$alias.'Trait')) { + continue; + } + + $class = new TraitBuilder('Symfony\Config', $alias); + $class->addMethod($alias, <<<'PHP' + /** + * @param COMMENT $config + */ + public function NAME(array $config): static + { + $this->NAMEConfig->configure($config); + + return $this; + } + PHP, [ + 'COMMENT' => ArrayShapeGenerator::generate($configuration->getConfigTreeBuilder()->buildTree()), + 'NAME' => $alias, + ]); + + $path = $this->getFullPath($class); + file_put_contents($path, $class->build()); + + $paths[] = $path; + } + + return function () use ($paths) { + foreach ($paths as $path) { + require_once $path; + } + }; + } + + private function getFullPath(ClassBuilder|TraitBuilder $class): string + { + $directory = $this->outputDir . \DIRECTORY_SEPARATOR . $class->getDirectory(); + if (!is_dir($directory)) { + @mkdir($directory, 0777, true); + } + + return $directory . \DIRECTORY_SEPARATOR . $class->getFilename(); + } +} diff --git a/src/Symfony/Component/Config/Builder/TraitBuilder.php b/src/Symfony/Component/Config/Builder/TraitBuilder.php new file mode 100644 index 0000000000000..404c258ab3a66 --- /dev/null +++ b/src/Symfony/Component/Config/Builder/TraitBuilder.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Builder; + +/** + * Build PHP classes to generate config. + * + * @internal + * + * @author Alexandre Daubois + */ +class TraitBuilder +{ + private string $name; + + /** @var Method[] */ + private array $methods = []; + private array $use = []; + + public function __construct( + private string $namespace, + string $name, + ) { + $this->name = ucfirst($this->camelCase($name)).'Trait'; + } + + public function getDirectory(): string + { + return str_replace('\\', \DIRECTORY_SEPARATOR, $this->namespace); + } + + public function getFilename(): string + { + return $this->name.'.php'; + } + + public function build(): string + { + $use = ''; + foreach (array_keys($this->use) as $statement) { + $use .= \sprintf('use %s;', $statement)."\n"; + } + + $body = ''; + foreach ($this->methods as $method) { + $lines = explode("\n", $method->getContent()); + foreach ($lines as $line) { + $body .= ($line ? ' '.$line : '')."\n"; + } + } + + return strtr(' $this->namespace, 'USE' => $use, 'TRAIT' => $this->getName(), 'BODY' => $body]); + } + + public function addUse(string $class): void + { + $this->use[$class] = true; + } + + public function addMethod(string $name, string $body, array $params = []): void + { + $this->methods[] = new Method(strtr($body, ['NAME' => $this->camelCase($name)] + $params)); + } + + private function camelCase(string $input): string + { + $output = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + + return preg_replace('#\W#', '', $output); + } + + public function getName(): string + { + return $this->name; + } + + public function getNamespace(): string + { + return $this->namespace; + } + + public function getFqcn(): string + { + return '\\'.$this->namespace.'\\'.$this->name; + } +} diff --git a/src/Symfony/Component/Config/Definition/NumericNode.php b/src/Symfony/Component/Config/Definition/NumericNode.php index a97850c9de746..eac160cf3de31 100644 --- a/src/Symfony/Component/Config/Definition/NumericNode.php +++ b/src/Symfony/Component/Config/Definition/NumericNode.php @@ -50,6 +50,16 @@ protected function finalizeValue(mixed $value): mixed return $value; } + public function getMin(): float|int|null + { + return $this->min; + } + + public function getMax(): float|int|null + { + return $this->max; + } + protected function isValueEmpty(mixed $value): bool { // a numeric value cannot be empty diff --git a/src/Symfony/Component/Config/Tests/Builder/ArrayShapeGeneratorTest.php b/src/Symfony/Component/Config/Tests/Builder/ArrayShapeGeneratorTest.php new file mode 100644 index 0000000000000..d487d1aebd435 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/ArrayShapeGeneratorTest.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Builder; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Builder\ArrayShapeGenerator; +use Symfony\Component\Config\Definition\ArrayNode; +use Symfony\Component\Config\Definition\BooleanNode; +use Symfony\Component\Config\Definition\EnumNode; +use Symfony\Component\Config\Definition\FloatNode; +use Symfony\Component\Config\Definition\IntegerNode; +use Symfony\Component\Config\Definition\NodeInterface; +use Symfony\Component\Config\Definition\PrototypedArrayNode; +use Symfony\Component\Config\Definition\ScalarNode; +use Symfony\Component\Config\Definition\StringNode; +use Symfony\Component\Config\Definition\VariableNode; + +class ArrayShapeGeneratorTest extends TestCase +{ + /** + * @dataProvider provideNodes + */ + public function testPhpDocHandlesNodeTypes(NodeInterface $node, string $expected) + { + $arrayNode = new ArrayNode('root'); + $arrayNode->addChild($node); + + $expected = 'node?: '.$expected; + + $this->assertStringContainsString($expected, ArrayShapeGenerator::generate($arrayNode)); + } + + public static function provideNodes(): iterable + { + yield [new ArrayNode('node'), 'array']; + + yield [new StringNode('node'), 'string']; + + yield [new BooleanNode('node'), 'bool']; + yield [new EnumNode('node', values: ['a', 'b']), '"a"|"b"']; + yield [new ScalarNode('node'), 'string|int|float|bool']; + yield [new VariableNode('node'), 'mixed']; + + yield [new IntegerNode('node'), 'int']; + yield [new IntegerNode('node', min: 1), 'int<1, max>']; + yield [new IntegerNode('node', max: 10), 'int']; + yield [new IntegerNode('node', min: 1, max: 10), 'int<1, 10>']; + + yield [new FloatNode('node'), 'float']; + yield [new FloatNode('node', min: 1.1), 'float']; + yield [new FloatNode('node', max: 10.1), 'float']; + yield [new FloatNode('node', min: 1.1, max: 10.1), 'float']; + } + + public function testPrototypedArrayNodePhpDoc() + { + $prototype = new PrototypedArrayNode('proto'); + $prototype->setPrototype(new StringNode('child')); + + $root = new ArrayNode('root'); + $root->addChild($prototype); + + $expected = "array{\n * proto?: array,\n * }"; + + $this->assertStringContainsString($expected, ArrayShapeGenerator::generate($root)); + } + + public function testPrototypedArrayNodePhpDocWithKeyAttribute() + { + $prototype = new PrototypedArrayNode('proto'); + $prototype->setPrototype(new StringNode('child')); + $prototype->setKeyAttribute('name'); + + $root = new ArrayNode('root'); + $root->addChild($prototype); + + $expected = "array{\n * proto?: array,\n * }"; + + $this->assertStringContainsString($expected, ArrayShapeGenerator::generate($root)); + } + + public function testPhpDocHandlesRequiredNode() + { + $child = new BooleanNode('node'); + $child->setRequired(true); + + $root = new ArrayNode('root'); + $root->addChild($child); + + $expected = 'node: bool'; + + $this->assertStringContainsString($expected, ArrayShapeGenerator::generate($root)); + } + + public function testPhpDocHandleAdditionalDocumentation() + { + $child = new BooleanNode('node'); + $child->setDeprecated('vendor/package', '1.0', 'The "%path%" option is deprecated.'); + $child->setDefaultValue(true); + $child->setInfo('This is a boolean node.'); + + $root = new ArrayNode('root'); + $root->addChild($child); + + $this->assertStringContainsString('node?: bool, // Deprecated: The "node" option is deprecated. This is a boolean node. Default: true', ArrayShapeGenerator::generate($root)); + } + + public function testPhpDocHandleMultilineDoc() + { + $child = new BooleanNode('node'); + $child->setDeprecated('vendor/package', '1.0', 'The "%path%" option is deprecated.'); + $child->setDefaultValue(true); + $child->setInfo("This is a boolean node.\nSet to true to enable it.\r\nSet to false to disable it."); + + $root = new ArrayNode('root'); + $root->addChild($child); + + $this->assertStringContainsString('node?: bool, // Deprecated: The "node" option is deprecated. This is a boolean node. Set to true to enable it. Set to false to disable it. Default: true', ArrayShapeGenerator::generate($root)); + } + + public function testPhpDocShapeSingleLevel() + { + $root = new ArrayNode('root'); + + $this->assertStringMatchesFormat('array<%s>', ArrayShapeGenerator::generate($root)); + } + + public function testPhpDocShapeMultiLevel() + { + $root = new ArrayNode('root'); + $child = new ArrayNode('child'); + $root->addChild($child); + + $this->assertStringMatchesFormat('array{%Achild?: array<%s>,%A}', ArrayShapeGenerator::generate($root)); + } + + /** + * @dataProvider provideQuotedNodes + */ + public function testPhpdocQuoteNodeName(NodeInterface $node, string $expected) + { + $arrayNode = new ArrayNode('root'); + $arrayNode->addChild($node); + + $this->assertStringContainsString($expected, ArrayShapeGenerator::generate($arrayNode)); + } + + public static function provideQuotedNodes(): \Generator + { + yield [new StringNode('int'), "'int'"]; + yield [new StringNode('float'), "'float'"]; + yield [new StringNode('null'), "'null'"]; + yield [new StringNode('bool'), "'bool'"]; + yield [new StringNode('scalar'), "'scalar'"]; + yield [new StringNode('hell"o'), "'hell\\\"o'"]; + yield [new StringNode("hell'o"), "'hell\\'o'"]; + yield [new StringNode('@key'), "'@key'"]; + } +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.config_function_output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.config_function_output.php new file mode 100644 index 0000000000000..ba0de03563448 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.config_function_output.php @@ -0,0 +1,29 @@ +addToListConfig = new AddToListConfig(); + $this->builders = [$this->addToListConfig]; + } + public function getBuilders(): array + { + return $this->builders; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToListConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToListConfig.php index a8b0adb28b5f2..bd5eac9c79945 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToListConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToListConfig.php @@ -14,6 +14,7 @@ class AddToListConfig implements \Symfony\Component\Config\Builder\ConfigBuilder { private $translator; private $messenger; + private $configOutput = []; private $_usedProperties = []; public function translator(array $value = []): \Symfony\Config\AddToList\TranslatorConfig @@ -40,6 +41,33 @@ public function messenger(array $value = []): \Symfony\Config\AddToList\Messenge return $this->messenger; } + public function configure( + /** @var array{ + * translator?: array{ + * fallbacks?: array, + * sources?: array, + * books?: array{ // Deprecated: The child node "books" at path "add_to_list.translator.books" is deprecated. looks for translation in old fashion way + * page?: array, + * content?: string|int|float|bool, + * }>, + * }, + * }, + * messenger?: array{ + * routing?: array, + * }>, + * receiving?: array, + * color?: string|int|float|bool, + * }>, + * }, + * } $config */ + array $config = []): void + { + $this->configOutput = $config; + } + public function getExtensionAlias(): string { return 'add_to_list'; @@ -66,6 +94,10 @@ public function __construct(array $value = []) public function toArray(): array { + if ($this->configOutput) { + return $this->configOutput; + } + $output = []; if (isset($this->_usedProperties['translator'])) { $output['translator'] = $this->translator->toArray(); diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.config_function_output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.config_function_output.php new file mode 100644 index 0000000000000..8c29903d8a899 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.config_function_output.php @@ -0,0 +1,29 @@ +arrayExtraKeysConfig = new ArrayExtraKeysConfig(); + $this->builders = [$this->arrayExtraKeysConfig]; + } + public function getBuilders(): array + { + return $this->builders; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeysConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeysConfig.php index 27e44233e735d..f6ed3e0dfe0b2 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeysConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeysConfig.php @@ -16,6 +16,7 @@ class ArrayExtraKeysConfig implements \Symfony\Component\Config\Builder\ConfigBu private $foo; private $bar; private $baz; + private $configOutput = []; private $_usedProperties = []; public function foo(array $value = []): \Symfony\Config\ArrayExtraKeys\FooConfig @@ -49,6 +50,23 @@ public function baz(array $value = []): \Symfony\Config\ArrayExtraKeys\BazConfig return $this->baz; } + public function configure( + /** @var array{ + * foo?: array{ + * baz?: string|int|float|bool, + * qux?: string|int|float|bool, + * }, + * bar?: array, + * baz?: array, + * } $config */ + array $config = []): void + { + $this->configOutput = $config; + } + public function getExtensionAlias(): string { return 'array_extra_keys'; @@ -81,6 +99,10 @@ public function __construct(array $value = []) public function toArray(): array { + if ($this->configOutput) { + return $this->configOutput; + } + $output = []; if (isset($this->_usedProperties['foo'])) { $output['foo'] = $this->foo->toArray(); diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.config_function_output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.config_function_output.php new file mode 100644 index 0000000000000..75d2bc00941ad --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.config_function_output.php @@ -0,0 +1,29 @@ +nodeInitialValuesConfig = new NodeInitialValuesConfig(); + $this->builders = [$this->nodeInitialValuesConfig]; + } + public function getBuilders(): array + { + return $this->builders; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValuesConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValuesConfig.php index d019ecf81bb76..3eb09ae570f28 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValuesConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValuesConfig.php @@ -14,6 +14,7 @@ class NodeInitialValuesConfig implements \Symfony\Component\Config\Builder\Confi { private $someCleverName; private $messenger; + private $configOutput = []; private $_usedProperties = []; public function someCleverName(array $value = []): \Symfony\Config\NodeInitialValues\SomeCleverNameConfig @@ -40,6 +41,26 @@ public function messenger(array $value = []): \Symfony\Config\NodeInitialValues\ return $this->messenger; } + public function configure( + /** @var array{ + * some_clever_name?: array{ + * first?: string|int|float|bool, + * second?: string|int|float|bool, + * third?: string|int|float|bool, + * }, + * messenger?: array{ + * transports?: array, + * }>, + * }, + * } $config */ + array $config = []): void + { + $this->configOutput = $config; + } + public function getExtensionAlias(): string { return 'node_initial_values'; @@ -66,6 +87,10 @@ public function __construct(array $value = []) public function toArray(): array { + if ($this->configOutput) { + return $this->configOutput; + } + $output = []; if (isset($this->_usedProperties['someCleverName'])) { $output['some_clever_name'] = $this->someCleverName->toArray(); diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config_function_output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config_function_output.php new file mode 100644 index 0000000000000..7f445f9a0f4aa --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config_function_output.php @@ -0,0 +1,29 @@ +placeholdersConfig = new PlaceholdersConfig(); + $this->builders = [$this->placeholdersConfig]; + } + public function getBuilders(): array + { + return $this->builders; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders/Symfony/Config/PlaceholdersConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders/Symfony/Config/PlaceholdersConfig.php index eb23423fffe3b..f7cc6a3bc4da1 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders/Symfony/Config/PlaceholdersConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders/Symfony/Config/PlaceholdersConfig.php @@ -13,6 +13,7 @@ class PlaceholdersConfig implements \Symfony\Component\Config\Builder\ConfigBuil private $enabled; private $favoriteFloat; private $goodIntegers; + private $configOutput = []; private $_usedProperties = []; /** @@ -54,6 +55,17 @@ public function goodIntegers(ParamConfigurator|array $value): static return $this; } + public function configure( + /** @var array{ + * enabled?: bool, // Default: false + * favorite_float?: float, + * good_integers?: array>, + * } $config */ + array $config = []): void + { + $this->configOutput = $config; + } + public function getExtensionAlias(): string { return 'placeholders'; @@ -86,6 +98,10 @@ public function __construct(array $value = []) public function toArray(): array { + if ($this->configOutput) { + return $this->configOutput; + } + $output = []; if (isset($this->_usedProperties['enabled'])) { $output['enabled'] = $this->enabled; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.config_function_output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.config_function_output.php new file mode 100644 index 0000000000000..bc48c11d9062f --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.config_function_output.php @@ -0,0 +1,29 @@ +primitiveTypesConfig = new PrimitiveTypesConfig(); + $this->builders = [$this->primitiveTypesConfig]; + } + public function getBuilders(): array + { + return $this->builders; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes/Symfony/Config/PrimitiveTypesConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes/Symfony/Config/PrimitiveTypesConfig.php index 4208b97dde1bc..d5b10d1276db7 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes/Symfony/Config/PrimitiveTypesConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes/Symfony/Config/PrimitiveTypesConfig.php @@ -18,6 +18,7 @@ class PrimitiveTypesConfig implements \Symfony\Component\Config\Builder\ConfigBu private $integerNode; private $scalarNode; private $scalarNodeWithDefault; + private $configOutput = []; private $_usedProperties = []; /** @@ -124,6 +125,22 @@ public function scalarNodeWithDefault($value): static return $this; } + public function configure( + /** @var array{ + * boolean_node?: bool, + * enum_node?: "foo"|"bar"|"baz"|Symfony\Component\Config\Tests\Fixtures\TestEnum::Bar, + * fqcn_enum_node?: foo|bar, + * fqcn_unit_enum_node?: Symfony\Component\Config\Tests\Fixtures\TestEnum::Foo|Symfony\Component\Config\Tests\Fixtures\TestEnum::Bar|Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc, + * float_node?: float, + * integer_node?: int, + * scalar_node?: string|int|float|bool, + * scalar_node_with_default?: string|int|float|bool, // Default: true + * } $config */ + array $config = []): void + { + $this->configOutput = $config; + } + public function getExtensionAlias(): string { return 'primitive_types'; @@ -186,6 +203,10 @@ public function __construct(array $value = []) public function toArray(): array { + if ($this->configOutput) { + return $this->configOutput; + } + $output = []; if (isset($this->_usedProperties['booleanNode'])) { $output['boolean_node'] = $this->booleanNode; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config_function_output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config_function_output.php new file mode 100644 index 0000000000000..f5e467c7ac651 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config_function_output.php @@ -0,0 +1,29 @@ +scalarNormalizedTypesConfig = new ScalarNormalizedTypesConfig(); + $this->builders = [$this->scalarNormalizedTypesConfig]; + } + public function getBuilders(): array + { + return $this->builders; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php index 1794ede72e18c..129f356ed3d1f 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php @@ -21,6 +21,7 @@ class ScalarNormalizedTypesConfig implements \Symfony\Component\Config\Builder\C private $listObject; private $keyedListObject; private $nested; + private $configOutput = []; private $_usedProperties = []; /** @@ -128,6 +129,37 @@ public function nested(array $value = []): \Symfony\Config\ScalarNormalizedTypes return $this->nested; } + public function configure( + /** @var array{ + * simple_array?: array, + * keyed_array?: array>, + * object?: array{ + * enabled?: bool, // Default: null + * date_format?: string|int|float|bool, + * remove_used_context_fields?: bool, + * }, + * list_object: array, + * }>, + * keyed_list_object?: array, + * }>, + * nested?: array{ + * nested_object?: array{ + * enabled?: bool, // Default: null + * }, + * nested_list_object?: array, + * }, + * } $config */ + array $config = []): void + { + $this->configOutput = $config; + } + public function getExtensionAlias(): string { return 'scalar_normalized_types'; @@ -178,6 +210,10 @@ public function __construct(array $value = []) public function toArray(): array { + if ($this->configOutput) { + return $this->configOutput; + } + $output = []; if (isset($this->_usedProperties['simpleArray'])) { $output['simple_array'] = $this->simpleArray; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.config_function_output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.config_function_output.php new file mode 100644 index 0000000000000..726a862c60598 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.config_function_output.php @@ -0,0 +1,29 @@ +variableTypeConfig = new VariableTypeConfig(); + $this->builders = [$this->variableTypeConfig]; + } + public function getBuilders(): array + { + return $this->builders; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType/Symfony/Config/VariableTypeConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType/Symfony/Config/VariableTypeConfig.php index 6cfd617932c82..288c61e875f78 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType/Symfony/Config/VariableTypeConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType/Symfony/Config/VariableTypeConfig.php @@ -11,6 +11,7 @@ class VariableTypeConfig implements \Symfony\Component\Config\Builder\ConfigBuilderInterface { private $anyValue; + private $configOutput = []; private $_usedProperties = []; /** @@ -27,6 +28,15 @@ public function anyValue(mixed $value): static return $this; } + public function configure( + /** @var array{ + * any_value?: mixed, + * } $config */ + array $config = []): void + { + $this->configOutput = $config; + } + public function getExtensionAlias(): string { return 'variable_type'; @@ -47,6 +57,10 @@ public function __construct(array $value = []) public function toArray(): array { + if ($this->configOutput) { + return $this->configOutput; + } + $output = []; if (isset($this->_usedProperties['anyValue'])) { $output['any_value'] = $this->anyValue; diff --git a/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php b/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php index 680010f00fc3c..3af984300b709 100644 --- a/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php +++ b/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php @@ -31,6 +31,7 @@ * * @covers \Symfony\Component\Config\Builder\ClassBuilder * @covers \Symfony\Component\Config\Builder\ConfigBuilderGenerator + * @covers \Symfony\Component\Config\Builder\ConfigTraitsGenerator * @covers \Symfony\Component\Config\Builder\Method * @covers \Symfony\Component\Config\Builder\Property */ @@ -76,7 +77,7 @@ public static function fixtureNames() /** * @dataProvider fixtureNames */ - public function testConfig(string $name, string $alias) + public function testConfigClass(string $name, string $alias) { $basePath = __DIR__.'/Fixtures/'; $callback = include $basePath.$name.'.config.php'; @@ -99,9 +100,24 @@ public function testConfig(string $name, string $alias) if (class_exists(AbstractConfigurator::class)) { $output = AbstractConfigurator::processValue($output); } + $this->assertSame($expectedOutput, $output); } + /** + * @dataProvider fixtureNames + */ + public function testConfigClassAndTraits(string $name, string $alias) + { + $basePath = __DIR__.'/Fixtures/'; + $expectedOutput = $basePath.$name.'.config_function_output.php'; + + $this->generateConfigClassBuilder($alias, 'Symfony\\Component\\Config\\Tests\\Builder\\Fixtures\\'.$name, $outputDir); + + file_put_contents($expectedOutput, file_get_contents($outputDir.'/Symfony/Config/Config.php')); + $this->assertFileEquals($expectedOutput, $outputDir.'/Symfony/Config/Config.php'); + } + /** * When you create a node, you can provide it with initial values. But the second * time you call a node, it is not created, hence you cannot give it initial values. @@ -177,6 +193,18 @@ private function generateConfigBuilder(string $configurationClass, ?string &$out return $loader(); } + private function generateConfigClassBuilder(string $alias, string $configurationClass, ?string &$outputDir = null) + { + $outputDir = tempnam(sys_get_temp_dir(), 'sf_config_builder_'); + unlink($outputDir); + mkdir($outputDir); + $this->tempDir[] = $outputDir; + + $configurationClass = new $configurationClass(); + + (new ConfigBuilderGenerator($outputDir))->buildConfigClassAndTraits([$alias => $configurationClass]); + } + private function assertDirectorySame($expected, $current) { $expectedFiles = []; diff --git a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php index 217eb7cc56faa..262e96b3bcc79 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php @@ -14,6 +14,7 @@ use Symfony\Component\Config\Builder\ConfigBuilderGenerator; use Symfony\Component\Config\Builder\ConfigBuilderGeneratorInterface; use Symfony\Component\Config\Builder\ConfigBuilderInterface; +use Symfony\Component\Config\Builder\ConfigClassAwareBuilderGeneratorInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\DependencyInjection\Attribute\When; use Symfony\Component\DependencyInjection\Attribute\WhenNot; @@ -24,6 +25,7 @@ use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Config\Config; /** * PhpFileLoader loads service definitions from a PHP file. @@ -36,6 +38,7 @@ class PhpFileLoader extends FileLoader { protected bool $autoRegisterAliasesForSinglyImplementedInterfaces = false; + private array $configurations = []; public function __construct( ContainerBuilder $container, @@ -57,16 +60,38 @@ public function load(mixed $resource, ?string $type = null): mixed $this->setCurrentDir(\dirname($path)); $this->container->fileExists($path); + if ($this->generator instanceof ConfigClassAwareBuilderGeneratorInterface) { + foreach ($this->container->getExtensions() as $extension) { + if (!$extension instanceof ConfigurationExtensionInterface) { + continue; + } + + $this->configurations[$extension->getAlias()] ??= $extension->getConfiguration([], $this->container); + + $this->generator->build($this->configurations[$extension->getAlias()])(); + } + + if (!class_exists(Config::class)) { + $this->generator->buildConfigClassAndTraits($this->configurations)(); + } + } + // the closure forbids access to the private scope in the included file - $load = \Closure::bind(function ($path, $env) use ($container, $loader, $resource, $type) { + $config = class_exists(Config::class) ? new Config($this->env) : null; + $load = \Closure::bind(function ($path, $env) use ($container, $loader, $resource, $type, $config) { return include $path; }, $this, ProtectedPhpFileLoader::class); try { $callback = $load($path, $this->env); + $containerConfigurator = new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env); if (\is_object($callback) && \is_callable($callback)) { - $this->executeCallback($callback, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path); + $this->executeCallback($callback, $containerConfigurator, $path); + } + + if (null !== $config && $this->generator) { + $this->processConfigClass($config, $containerConfigurator); } } finally { $this->instanceof = []; @@ -180,6 +205,37 @@ class_exists(ContainerConfigurator::class); $this->loadExtensionConfigs(); } + private function processConfigClass(Config $config, ContainerConfigurator $containerConfigurator): void + { + foreach ($config->getBuilders() as $configBuilder) { + $this->configBuilder($configBuilder::class); + + $this->loadExtensionConfig($configBuilder->getExtensionAlias(), ContainerConfigurator::processValue($configBuilder->toArray())); + } + + foreach ($config->getImports() as $import) { + if (\is_array($import)) { + $containerConfigurator->import($import['resource'], $import['type'] ?? null, $import['ignoreErrors'] ?? false); + } else { + $containerConfigurator->import($import); + } + } + + $parametersConfigurator = $containerConfigurator->parameters(); + foreach ($config->getParameters() as $key => $value) { + $parametersConfigurator->set($key, $value); + } + + $servicesConfigurator = $containerConfigurator->services(); + foreach ($config->getServices() as $id => $class) { + if (\is_array($class)) { + $servicesConfigurator->set($class['id'], $class['class'] ?? null); + } else { + $servicesConfigurator->set($id, $class); + } + } + } + /** * @param string $namespace FQCN string for a class implementing ConfigBuilderInterface */ @@ -220,7 +276,7 @@ private function configBuilder(string $namespace): ConfigBuilderInterface throw new \LogicException(\sprintf('You cannot use the config builder for "%s" because the extension does not implement "%s".', $namespace, ConfigurationExtensionInterface::class)); } - $configuration = $extension->getConfiguration([], $this->container); + $configuration = $this->configurations[$alias] ?? $extension->getConfiguration([], $this->container); $loader = $this->generator->build($configuration); return $loader(); diff --git a/src/Symfony/Component/DependencyInjection/Resource/ImportsTrait.php b/src/Symfony/Component/DependencyInjection/Resource/ImportsTrait.php new file mode 100644 index 0000000000000..34bf0da348448 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Resource/ImportsTrait.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Resource; + +trait ImportsTrait +{ + private array $imports = []; + + /** @var array $imports */ + public function imports(array $imports): static + { + $this->imports = $imports; + + return $this; + } + + public function getImports(): array + { + return $this->imports; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Resource/ParametersTrait.php b/src/Symfony/Component/DependencyInjection/Resource/ParametersTrait.php new file mode 100644 index 0000000000000..1733c15a64f72 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Resource/ParametersTrait.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Resource; + +trait ParametersTrait +{ + private array $parameters = []; + + /** + * @var array $parameters + */ + function parameters(array $parameters): static + { + $this->parameters = $parameters; + + return $this; + } + + public function getParameters(): array + { + return $this->parameters; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Resource/ServicesTrait.php b/src/Symfony/Component/DependencyInjection/Resource/ServicesTrait.php new file mode 100644 index 0000000000000..26b4ecdc52bd4 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Resource/ServicesTrait.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Resource; + +trait ServicesTrait +{ + private array $services = []; + + /** + * @var array|array $services + */ + public function services(array $services): static + { + $this->services = $services; + + return $this; + } + + public function getServices(): array + { + return $this->services; + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy