10000 [FrameworkBundle] Generate `Config` class by alexandre-daubois · Pull Request #58771 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[FrameworkBundle] Generate Config class #58771

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[FrameworkBundle] Generate configuration classes and traits
  • Loading branch information
alexandre-daubois committed Apr 19, 2025
commit 2990a6804cce4f5b9cb5f691a2881b719468bb88
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +29,7 @@
* Generate all config builders.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Alexandre Daubois <alex.daubois@gmail.com>
*
* @final since Symfony 7.1
*/
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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');
}
}
}

Expand Down
153 changes: 153 additions & 0 deletions src/Symfony/Component/Config/Builder/ArrayShapeGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <alex.daubois@gmail.com>
*
* @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<array-key, mixed>';
}

$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',
};
}
}
9 changes: 9 additions & 0 deletions src/Symfony/Component/Config/Builder/ClassBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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";
}
Expand Down Expand Up @@ -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, '\\');
Expand Down
Loading
Loading
0