diff --git a/src/Symfony/Component/Config/Builder/ClassBuilder.php b/src/Symfony/Component/Config/Builder/ClassBuilder.php index 9bf503ec5c..7e4f3a4972 100644 --- a/src/Symfony/Component/Config/Builder/ClassBuilder.php +++ b/src/Symfony/Component/Config/Builder/ClassBuilder.php @@ -32,6 +32,7 @@ class ClassBuilder /** @var Method[] */ private $methods = []; private $require = []; + private $use = []; private $implements = []; public function __construct(string $namespace, string $name) @@ -66,6 +67,10 @@ class ClassBuilder } $require .= sprintf('require_once __DIR__.\DIRECTORY_SEPARATOR.\'%s\';', implode('\'.\DIRECTORY_SEPARATOR.\'', $path))."\n"; } + $use = ''; + foreach (array_keys($this->use) as $statement) { + $use .= sprintf('use %s;', $statement)."\n"; + } $implements = [] === $this->implements ? '' : 'implements '.implode(', ', $this->implements); $body = ''; @@ -84,6 +89,7 @@ class ClassBuilder namespace NAMESPACE; REQUIRE +USE /** * This class is automatically generated to help creating config. @@ -94,17 +100,22 @@ class CLASS IMPLEMENTS { BODY } -', ['NAMESPACE' => $this->namespace, 'REQUIRE' => $require, 'CLASS' => $this->getName(), 'IMPLEMENTS' => $implements, 'BODY' => $body]); +', ['NAMESPACE' => $this->namespace, 'REQUIRE' => $require, 'USE' => $use, 'CLASS' => $this->getName(), 'IMPLEMENTS' => $implements, 'BODY' => $body]); return $content; } - public function addRequire(self $class) + public function addRequire(self $class): void { $this->require[] = $class; } - public function addImplements(string $interface) + public function addUse(string $class): void + { + $this->use[$class] = true; + } + + public function addImplements(string $interface): void { $this->implements[] = '\\'.ltrim($interface, '\\'); } @@ -148,7 +159,7 @@ BODY return $this->namespace; } - public function getFqcn() + public function getFqcn(): string { return '\\'.$this->namespace.'\\'.$this->name; } diff --git a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php index 63b309d786..fc36bb5feb 100644 --- a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php +++ b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php @@ -15,12 +15,14 @@ use Symfony\Component\Config\Definition\ArrayNode; use Symfony\Component\Config\Definition\BooleanNode; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\EnumNode; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; 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\VariableNode; +use Symfony\Component\Config\Loader\ParamConfigurator; /** * Generate ConfigBuilders to help create valid config. @@ -83,7 +85,7 @@ public function NAME(): string return $directory.\DIRECTORY_SEPARATOR.$class->getFilename(); } - private function writeClasses() + private function writeClasses(): void { foreach ($this->classes as $class) { $this->buildConstructor($class); @@ -95,7 +97,7 @@ public function NAME(): string $this->classes = []; } - private function buildNode(NodeInterface $node, ClassBuilder $class, string $namespace) + private function buildNode(NodeInterface $node, ClassBuilder $class, string $namespace): void { if (!$node instanceof ArrayNode) { throw new \LogicException('The node was expected to be an ArrayNode. This Configuration includes an edge case not supported yet.'); @@ -121,7 +123,7 @@ public function NAME(): string } } - private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $namespace) + private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $namespace): void { $childClass = new ClassBuilder($namespace, $node->getName()); $class->addRequire($childClass); @@ -134,20 +136,22 @@ public function NAME(array $value = []): CLASS if (null === $this->PROPERTY) { $this->PROPERTY = new CLASS($value); } elseif ([] !== $value) { - throw new \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException(sprintf(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\')); + throw new InvalidConfigurationException(sprintf(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\')); } return $this->PROPERTY; }'; + $class->addUse(InvalidConfigurationException::class); $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn()]); $this->buildNode($node, $childClass, $this->getSubNamespace($childClass)); } - private function handleVariableNode(VariableNode $node, ClassBuilder $class) + private function handleVariableNode(VariableNode $node, ClassBuilder $class): void { $comment = $this->getComment($node); $property = $class->addProperty($node->getName()); + $class->addUse(ParamConfigurator::class); $body = ' /** @@ -162,7 +166,7 @@ public function NAME($valueDEFAULT): self $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'COMMENT' => $comment, 'DEFAULT' => $node->hasDefaultValue() ? ' = '.var_export($node->getDefaultValue(), true) : '']); } - private function handlePrototypedArrayNode(PrototypedArrayNode $node, ClassBuilder $class, string $namespace) + private function handlePrototypedArrayNode(PrototypedArrayNode $node, ClassBuilder $class, string $namespace): void { $name = $this->getSingularName($node); $prototype = $node->getPrototype(); @@ -170,15 +174,16 @@ public function NAME($valueDEFAULT): self $parameterType = $this->getParameterType($prototype); if (null !== $parameterType || $prototype instanceof ScalarNode) { + $class->addUse(ParamConfigurator::class); $property = $class->addProperty($node->getName()); if (null === $key = $node->getKeyAttribute()) { // This is an array of values; don't use singular name $body = ' /** - * @param list $value + * @param ParamConfigurator|list $value * @return $this */ -public function NAME(array $value): self +public function NAME($value): self { $this->PROPERTY = $value; @@ -189,16 +194,17 @@ public function NAME(array $value): self } else { $body = ' /** + * @param ParamConfigurator|TYPE $value * @return $this */ -public function NAME(string $VAR, TYPE$VALUE): self +public function NAME(string $VAR, $VALUE): self { $this->PROPERTY[$VAR] = $VALUE; return $this; }'; - $class->addMethod($methodName, $body, ['PROPERTY' => $property->getName(), 'TYPE' => '' === $parameterType ? '' : $parameterType.' ', 'VAR' => '' === $key ? 'key' : $key, 'VALUE' => 'value' === $key ? 'data' : 'value']); + $class->addMethod($methodName, $body, ['PROPERTY' => $property->getName(), 'TYPE' => '' === $parameterType ? 'mixed' : $parameterType, 'VAR' => '' === $key ? 'key' : $key, 'VALUE' => 'value' === $key ? 'data' : 'value']); } return; @@ -227,31 +233,33 @@ public function NAME(string $VAR, array $VALUE = []): CLASS return $this->PROPERTY[$VAR]; } - throw new \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException(sprintf(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\')); + throw new InvalidConfigurationException(sprintf(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\')); }'; + $class->addUse(InvalidConfigurationException::class); $class->addMethod($methodName, $body, ['PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn(), 'VAR' => '' === $key ? 'key' : $key, 'VALUE' => 'value' === $key ? 'data' : 'value']); } $this->buildNode($prototype, $childClass, $namespace.'\\'.$childClass->getName()); } - private function handleScalarNode(ScalarNode $node, ClassBuilder $class) + private function handleScalarNode(ScalarNode $node, ClassBuilder $class): void { $comment = $this->getComment($node); $property = $class->addProperty($node->getName()); + $class->addUse(ParamConfigurator::class); $body = ' /** COMMENT * @return $this */ -public function NAME(TYPE$value): self +public function NAME($value): self { $this->PROPERTY = $value; return $this; }'; - $parameterType = $this->getParameterType($node) ?? ''; - $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'TYPE' => '' === $parameterType ? '' : $parameterType.' ', 'COMMENT' => $comment]); + + $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'COMMENT' => $comment]); } private function getParameterType(NodeInterface $node): ?string @@ -301,9 +309,15 @@ public function NAME(TYPE$value): self } if ($node instanceof EnumNode) { - $comment .= sprintf(' * @param %s $value', implode('|', array_map(function ($a) { + $comment .= sprintf(' * @param ParamConfigurator|%s $value', implode('|', array_map(function ($a) { return var_export($a, true); }, $node->getValues()))).\PHP_EOL; + } else { + $parameterType = $this->getParameterType($node); + if (null === $parameterType || '' === $parameterType) { + $parameterType = 'mixed'; + } + $comment .= ' * @param ParamConfigurator|'.$parameterType.' $value'.\PHP_EOL; } if ($node->isDeprecated()) { @@ -387,9 +401,10 @@ public function NAME(): array $body .= ' if ($value !== []) { - throw new \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException(sprintf(\'The following keys are not supported by "%s": \', __CLASS__) . implode(\', \', array_keys($value))); + throw new InvalidConfigurationException(sprintf(\'The following keys are not supported by "%s": \', __CLASS__) . implode(\', \', array_keys($value))); }'; + $class->addUse(InvalidConfigurationException::class); $class->addMethod('__construct', ' public function __construct(array $value = []) { diff --git a/src/Symfony/Component/Config/Loader/ParamConfigurator.php b/src/Symfony/Component/Config/Loader/ParamConfigurator.php new file mode 100644 index 0000000000..70c3f79a68 --- /dev/null +++ b/src/Symfony/Component/Config/Loader/ParamConfigurator.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\Config\Loader; + +/** + * Placeholder for a parameter. + * + * @author Tobias Nyholm + */ +class ParamConfigurator +{ + private $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function __toString(): string + { + return '%'.$this->name.'%'; + } +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config.php new file mode 100644 index 0000000000..3423f9466d --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config.php @@ -0,0 +1,10 @@ +enabled(env('FOO_ENABLED')->bool()); + $config->favoriteFloat(param('eulers_number')); + $config->goodIntegers(env('MY_INTEGERS')->json()); +}; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.output.php new file mode 100644 index 0000000000..ca27298c36 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.output.php @@ -0,0 +1,7 @@ + '%env(bool:FOO_ENABLED)%', + 'favorite_float' => '%eulers_number%', + 'good_integers' => '%env(json:MY_INTEGERS)%', +]; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.php new file mode 100644 index 0000000000..78baa47735 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.php @@ -0,0 +1,26 @@ +getRootNode(); + $rootNode + ->children() + ->booleanNode('enabled')->defaultFalse()->end() + ->floatNode('favorite_float')->end() + ->arrayNode('good_integers') + ->integerPrototype()->end() + ->end() + ->end() + ; + + return $tb; + } +} diff --git a/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php b/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php index c033d70086..4c0ace8d01 100644 --- a/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php +++ b/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php @@ -9,6 +9,8 @@ use Symfony\Component\Config\Builder\ConfigBuilderInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Tests\Builder\Fixtures\AddToList; use Symfony\Component\Config\Tests\Builder\Fixtures\NodeInitialValues; +use Symfony\Component\DependencyInjection\Loader\Configurator\AbstractConfigurator; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Config\AddToListConfig; /** @@ -30,6 +32,14 @@ class GeneratedConfigTest extends TestCase foreach ($array as $name => $alias) { yield $name => [$name, $alias]; } + + /* + * Force load ContainerConfigurator to make env(), param() etc available + * and also check if symfony/dependency-injection is installed + */ + if (class_exists(ContainerConfigurator::class)) { + yield 'Placeholders' => ['Placeholders', 'placeholders']; + } } /** @@ -45,7 +55,11 @@ class GeneratedConfigTest extends TestCase $this->assertInstanceOf(ConfigBuilderInterface::class, $configBuilder); $this->assertSame($alias, $configBuilder->getExtensionAlias()); - $this->assertSame($expectedOutput, $configBuilder->toArray()); + $output = $configBuilder->toArray(); + if (class_exists(AbstractConfigurator::class)) { + $output = AbstractConfigurator::processValue($output); + } + $this->assertSame($expectedOutput, $output); } /** diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php index 93d84337d0..788d2459d7 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Definition; @@ -83,7 +84,7 @@ abstract class AbstractConfigurator return $def; } - if ($value instanceof EnvConfigurator) { + if ($value instanceof ParamConfigurator) { return (string) $value; } diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php index 0cafaa2037..9ea1dfec4d 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; @@ -96,9 +97,9 @@ class ContainerConfigurator extends AbstractConfigurator /** * Creates a parameter. */ -function param(string $name): string +function param(string $name): ParamConfigurator { - return '%'.$name.'%'; + return new ParamConfigurator($name); } /** diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/EnvConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/EnvConfigurator.php index 21d9b84df1..d1864f564a 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/EnvConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/EnvConfigurator.php @@ -11,7 +11,9 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -class EnvConfigurator +use Symfony\Component\Config\Loader\ParamConfigurator; + +class EnvConfigurator extends ParamConfigurator { /** * @var string[] @@ -23,10 +25,7 @@ class EnvConfigurator $this->stack = explode(':', $name); } - /** - * @return string - */ - public function __toString() + public function __toString(): string { return '%env('.implode(':', $this->stack).')%'; } diff --git a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php index f10aaac4a1..a777653140 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php @@ -128,6 +128,9 @@ class PhpFileLoader extends FileLoader } } + // Force load ContainerConfigurator to make env(), param() etc available. + class_exists(ContainerConfigurator::class); + $callback(...$arguments); /** @var ConfigBuilderInterface $configBuilder */