[Config] Builder: Remove typehints and allow for EnvConfigurator

This commit is contained in:
Nyholm 2021-04-22 10:37:03 +02:00
parent 84a514c4b1
commit 59b79d35a7
No known key found for this signature in database
GPG Key ID: D6332DE2B6F8FA38
11 changed files with 149 additions and 30 deletions

View File

@ -32,6 +32,7 @@ class ClassBuilder
/** @var Method[] */ /** @var Method[] */
private $methods = []; private $methods = [];
private $require = []; private $require = [];
private $use = [];
private $implements = []; private $implements = [];
public function __construct(string $namespace, string $name) 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"; $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); $implements = [] === $this->implements ? '' : 'implements '.implode(', ', $this->implements);
$body = ''; $body = '';
@ -84,6 +89,7 @@ class ClassBuilder
namespace NAMESPACE; namespace NAMESPACE;
REQUIRE REQUIRE
USE
/** /**
* This class is automatically generated to help creating config. * This class is automatically generated to help creating config.
@ -94,17 +100,22 @@ class CLASS IMPLEMENTS
{ {
BODY 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; return $content;
} }
public function addRequire(self $class) public function addRequire(self $class): void
{ {
$this->require[] = $class; $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, '\\'); $this->implements[] = '\\'.ltrim($interface, '\\');
} }
@ -148,7 +159,7 @@ BODY
return $this->namespace; return $this->namespace;
} }
public function getFqcn() public function getFqcn(): string
{ {
return '\\'.$this->namespace.'\\'.$this->name; return '\\'.$this->namespace.'\\'.$this->name;
} }

View File

@ -15,12 +15,14 @@ use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\BooleanNode; use Symfony\Component\Config\Definition\BooleanNode;
use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\EnumNode; use Symfony\Component\Config\Definition\EnumNode;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\FloatNode; use Symfony\Component\Config\Definition\FloatNode;
use Symfony\Component\Config\Definition\IntegerNode; use Symfony\Component\Config\Definition\IntegerNode;
use Symfony\Component\Config\Definition\NodeInterface; use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode; use Symfony\Component\Config\Definition\PrototypedArrayNode;
use Symfony\Component\Config\Definition\ScalarNode; use Symfony\Component\Config\Definition\ScalarNode;
use Symfony\Component\Config\Definition\VariableNode; use Symfony\Component\Config\Definition\VariableNode;
use Symfony\Component\Config\Loader\ParamConfigurator;
/** /**
* Generate ConfigBuilders to help create valid config. * Generate ConfigBuilders to help create valid config.
@ -83,7 +85,7 @@ public function NAME(): string
return $directory.\DIRECTORY_SEPARATOR.$class->getFilename(); return $directory.\DIRECTORY_SEPARATOR.$class->getFilename();
} }
private function writeClasses() private function writeClasses(): void
{ {
foreach ($this->classes as $class) { foreach ($this->classes as $class) {
$this->buildConstructor($class); $this->buildConstructor($class);
@ -95,7 +97,7 @@ public function NAME(): string
$this->classes = []; $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) { if (!$node instanceof ArrayNode) {
throw new \LogicException('The node was expected to be an ArrayNode. This Configuration includes an edge case not supported yet.'); 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()); $childClass = new ClassBuilder($namespace, $node->getName());
$class->addRequire($childClass); $class->addRequire($childClass);
@ -134,20 +136,22 @@ public function NAME(array $value = []): CLASS
if (null === $this->PROPERTY) { if (null === $this->PROPERTY) {
$this->PROPERTY = new CLASS($value); $this->PROPERTY = new CLASS($value);
} elseif ([] !== $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; return $this->PROPERTY;
}'; }';
$class->addUse(InvalidConfigurationException::class);
$class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn()]); $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn()]);
$this->buildNode($node, $childClass, $this->getSubNamespace($childClass)); $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); $comment = $this->getComment($node);
$property = $class->addProperty($node->getName()); $property = $class->addProperty($node->getName());
$class->addUse(ParamConfigurator::class);
$body = ' $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) : '']); $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); $name = $this->getSingularName($node);
$prototype = $node->getPrototype(); $prototype = $node->getPrototype();
@ -170,15 +174,16 @@ public function NAME($valueDEFAULT): self
$parameterType = $this->getParameterType($prototype); $parameterType = $this->getParameterType($prototype);
if (null !== $parameterType || $prototype instanceof ScalarNode) { if (null !== $parameterType || $prototype instanceof ScalarNode) {
$class->addUse(ParamConfigurator::class);
$property = $class->addProperty($node->getName()); $property = $class->addProperty($node->getName());
if (null === $key = $node->getKeyAttribute()) { if (null === $key = $node->getKeyAttribute()) {
// This is an array of values; don't use singular name // This is an array of values; don't use singular name
$body = ' $body = '
/** /**
* @param list<TYPE> $value * @param ParamConfigurator|list<TYPE|ParamConfigurator> $value
* @return $this * @return $this
*/ */
public function NAME(array $value): self public function NAME($value): self
{ {
$this->PROPERTY = $value; $this->PROPERTY = $value;
@ -189,16 +194,17 @@ public function NAME(array $value): self
} else { } else {
$body = ' $body = '
/** /**
* @param ParamConfigurator|TYPE $value
* @return $this * @return $this
*/ */
public function NAME(string $VAR, TYPE$VALUE): self public function NAME(string $VAR, $VALUE): self
{ {
$this->PROPERTY[$VAR] = $VALUE; $this->PROPERTY[$VAR] = $VALUE;
return $this; 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; return;
@ -227,31 +233,33 @@ public function NAME(string $VAR, array $VALUE = []): CLASS
return $this->PROPERTY[$VAR]; 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']); $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()); $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); $comment = $this->getComment($node);
$property = $class->addProperty($node->getName()); $property = $class->addProperty($node->getName());
$class->addUse(ParamConfigurator::class);
$body = ' $body = '
/** /**
COMMENT * @return $this COMMENT * @return $this
*/ */
public function NAME(TYPE$value): self public function NAME($value): self
{ {
$this->PROPERTY = $value; $this->PROPERTY = $value;
return $this; 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 private function getParameterType(NodeInterface $node): ?string
@ -301,9 +309,15 @@ public function NAME(TYPE$value): self
} }
if ($node instanceof EnumNode) { 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); return var_export($a, true);
}, $node->getValues()))).\PHP_EOL; }, $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()) { if ($node->isDeprecated()) {
@ -387,9 +401,10 @@ public function NAME(): array
$body .= ' $body .= '
if ($value !== []) { 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', ' $class->addMethod('__construct', '
public function __construct(array $value = []) public function __construct(array $value = [])
{ {

View File

@ -0,0 +1,32 @@
<?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\Loader;
/**
* Placeholder for a parameter.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ParamConfigurator
{
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function __toString(): string
{
return '%'.$this->name.'%';
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Config\PlaceholdersConfig;
return static function (PlaceholdersConfig $config) {
$config->enabled(env('FOO_ENABLED')->bool());
$config->favoriteFloat(param('eulers_number'));
$config->goodIntegers(env('MY_INTEGERS')->json());
};

View File

@ -0,0 +1,7 @@
<?php
return [
'enabled' => '%env(bool:FOO_ENABLED)%',
'favorite_float' => '%eulers_number%',
'good_integers' => '%env(json:MY_INTEGERS)%',
];

View File

@ -0,0 +1,26 @@
<?php
namespace Symfony\Component\Config\Tests\Builder\Fixtures;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Placeholders implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$tb = new TreeBuilder('placeholders');
$rootNode = $tb->getRootNode();
$rootNode
->children()
->booleanNode('enabled')->defaultFalse()->end()
->floatNode('favorite_float')->end()
->arrayNode('good_integers')
->integerPrototype()->end()
->end()
->end()
;
return $tb;
}
}

View File

@ -9,6 +9,8 @@ use Symfony\Component\Config\Builder\ConfigBuilderInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Tests\Builder\Fixtures\AddToList; use Symfony\Component\Config\Tests\Builder\Fixtures\AddToList;
use Symfony\Component\Config\Tests\Builder\Fixtures\NodeInitialValues; 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; use Symfony\Config\AddToListConfig;
/** /**
@ -30,6 +32,14 @@ class GeneratedConfigTest extends TestCase
foreach ($array as $name => $alias) { foreach ($array as $name => $alias) {
yield $name => [$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->assertInstanceOf(ConfigBuilderInterface::class, $configBuilder);
$this->assertSame($alias, $configBuilder->getExtensionAlias()); $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);
} }
/** /**

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator; namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\Config\Loader\ParamConfigurator;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
@ -83,7 +84,7 @@ abstract class AbstractConfigurator
return $def; return $def;
} }
if ($value instanceof EnvConfigurator) { if ($value instanceof ParamConfigurator) {
return (string) $value; return (string) $value;
} }

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator; namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\Config\Loader\ParamConfigurator;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
@ -96,9 +97,9 @@ class ContainerConfigurator extends AbstractConfigurator
/** /**
* Creates a parameter. * Creates a parameter.
*/ */
function param(string $name): string function param(string $name): ParamConfigurator
{ {
return '%'.$name.'%'; return new ParamConfigurator($name);
} }
/** /**

View File

@ -11,7 +11,9 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator; namespace Symfony\Component\DependencyInjection\Loader\Configurator;
class EnvConfigurator use Symfony\Component\Config\Loader\ParamConfigurator;
class EnvConfigurator extends ParamConfigurator
{ {
/** /**
* @var string[] * @var string[]
@ -23,10 +25,7 @@ class EnvConfigurator
$this->stack = explode(':', $name); $this->stack = explode(':', $name);
} }
/** public function __toString(): string
* @return string
*/
public function __toString()
{ {
return '%env('.implode(':', $this->stack).')%'; return '%env('.implode(':', $this->stack).')%';
} }

View File

@ -128,6 +128,9 @@ class PhpFileLoader extends FileLoader
} }
} }
// Force load ContainerConfigurator to make env(), param() etc available.
class_exists(ContainerConfigurator::class);
$callback(...$arguments); $callback(...$arguments);
/** @var ConfigBuilderInterface $configBuilder */ /** @var ConfigBuilderInterface $configBuilder */