Skip to content

[Config] Builder: Remove typehints and allow for EnvConfigurator #40903

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

Merged
merged 1 commit into from
Apr 26, 2021
Merged
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
19 changes: 15 additions & 4 deletions src/Symfony/Component/Config/Builder/ClassBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ClassBuilder
/** @var Method[] */
private $methods = [];
private $require = [];
private $use = [];
private $implements = [];

public function __construct(string $namespace, string $name)
Expand Down Expand Up @@ -66,6 +67,10 @@ public function build(): string
}
$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 = '';
Expand All @@ -84,6 +89,7 @@ public function build(): string
namespace NAMESPACE;

REQUIRE
USE

/**
* This class is automatically generated to help creating config.
Expand All @@ -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, '\\');
}
Expand Down Expand Up @@ -148,7 +159,7 @@ public function getNamespace(): string
return $this->namespace;
}

public function getFqcn()
public function getFqcn(): string
{
return '\\'.$this->namespace.'\\'.$this->name;
}
Expand Down
49 changes: 32 additions & 17 deletions src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
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.
Expand Down Expand Up @@ -83,7 +85,7 @@ private function getFullPath(ClassBuilder $class): string
return $directory.\DIRECTORY_SEPARATOR.$class->getFilename();
}

private function writeClasses()
private function writeClasses(): void
{
foreach ($this->classes as $class) {
$this->buildConstructor($class);
Expand All @@ -95,7 +97,7 @@ private function writeClasses()
$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.');
Expand All @@ -121,7 +123,7 @@ private function buildNode(NodeInterface $node, ClassBuilder $class, string $nam
}
}

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);
Expand All @@ -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 = '
/**
Expand All @@ -162,23 +166,24 @@ 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();
$methodName = $name;

$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<TYPE> $value
* @param ParamConfigurator|list<TYPE|ParamConfigurator> $value
* @return $this
*/
public function NAME(array $value): self
public function NAME($value): self
{
$this->PROPERTY = $value;

Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -301,9 +309,15 @@ private function getComment(VariableNode $node): string
}

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()) {
Expand Down Expand Up @@ -387,9 +401,10 @@ private function buildConstructor(ClassBuilder $class): void

$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 = [])
{
Expand Down
32 changes: 32 additions & 0 deletions src/Symfony/Component/Config/Loader/ParamConfigurator.php
Original file line number Diff line number Diff line change
@@ -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.'%';
}
}
Original file line number Diff line number Diff line change
@@ -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());
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

return [
'enabled' => '%env(bool:FOO_ENABLED)%',
'favorite_float' => '%eulers_number%',
'good_integers' => '%env(json:MY_INTEGERS)%',
];
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
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;

/**
Expand All @@ -30,6 +32,14 @@ public function fixtureNames()
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'];
}
}

/**
Expand All @@ -45,7 +55,11 @@ public function testConfig(string $name, string $alias)

$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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,7 +84,7 @@ public static function processValue($value, $allowServices = false)
return $def;
}

if ($value instanceof EnvConfigurator) {
if ($value instanceof ParamConfigurator) {
return (string) $value;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,9 +97,9 @@ final public function withPath(string $path): self
/**
* Creates a parameter.
*/
function param(string $name): string
function param(string $name): ParamConfigurator
{
return '%'.$name.'%';
return new ParamConfigurator($name);
}

/**
Expand Down
Loading