Description
Description
Defining configuration trees can be a bit cumbersome and syntactically verbose, and in some non-intuitive for some combinations of configuration.
Let's look a seemingly simple configuration file:
my_package:
string_key: 'abc'
int_key: 1
dict_key:
a: 1
b: 2
list_key: [1, 2, 3]
list_of_dict_key:
- a: 1
b: 2
dict_of_list:
a: ['', '']
b: [0, 0]
To define this with the current tree builder syntax, it would look like:
Current Syntax
$treeBuilder = new TreeBuilder();
$treeBuilder->root('my_package')
->children()
->scalarNode('string_key')->end()
->integerNode('int_key')->end()
->arrayNode('dict_key')
->children()
->scalarNode('a')->end()
->integerNode('b')->end()
->end()
->end()
->arrayNode('list_key')
->integerPrototype()->end()
->end()
->arrayNode('list_o
8000
f_dict_key')
->arrayPrototype()
->children()
->integerNode('a')->end()
->integerNode('b')->end()
->end()
->end()
->end()
->arrayNode('dict_of_list_key')
->children()
->arrayNode('a')
->scalarPrototype()->end()
->end()
->arrayNode('b')
->integerPrototype()->end()
->end()
->end()
->end()
->end();
return $treeBuilder;
Syntax aside, I still find myself struggling to write the proper code for validation (even with an IDE) because it's very easy to miss an end()
call which then messes up downstream definitions, and this can cascade if you have large config trees.
I think this is a great example where using a more declarative composition-friendly interface would make declaring configuration schemas easier to maintain, learn, and understand.
Declarative Syntax
return configTree('my_package', dict([
'string_key' => string(),
'int_key' => int(),
'dict_key' => dict([
'a' => int(),
'b' => int(),
]),
'list_key' => listOf(int()),
'list_of_dict_key' => listOf(dict([
'a' => int(),
'b' => int(),
])),
'dict_of_list_key' => dict([
'a' => listOf(string()),
'b' => listOf(int()),
])
]));
IMHO, this syntax is much easier to read and understand the structure of what we are configuring and is less error prone to missing an end
statement.
It also is pretty easy to allow access to the actual node definitions via the configure
method like:
dict([])->configure(fn(ArrayNodeDefinition $node) => $node->canBeEnabled()->ignoreExtraKeys(false));
Rough Implementation
The following code is a working implementation for this declarative syntax, the formatting is rough, but if this RFC is received well, I could clean this up into a pull request.
abstract class ConfigNode {
protected $attributes;
public function __construct(array $attributes = []) {
$this->attributes = $attributes;
}
public function attribute(string $key) {
return $this->attributes[$key] ?? null;
}
public function attributes(): array {
return $this->attributes;
}
public function withAttributes(array $attributes): self {
$self = clone $this;
$self->attributes = $attributes;
return $self;
}
public function withAddedAttributes(array $addedAttributes): self {
return $this->withAttributes(array_merge($this->attributes, $addedAttributes));
}
public function name(): ?string {
return $this->attribute('name');
}
public function configure(callable $configure): self {
return $this->withAddedAttributes(['configure' => $configure]);
}
public function optionallyConfigure($node): void {
$configure = $this->attribute('configure');
if ($configure) {
$configure($node);
}
}
}
final class DictNode extends ConfigNode {
/** @return ConfigNode[] */
public function nodesByName(): array {
return $this->attributes['nodesByName'] ?? [];
}
}
final class ListOfNode extends ConfigNode {
public function node(): ?ConfigNode {
return $this->attribute('node');
}
}
final class StringNode extends ConfigNode {}
final class IntNode extends ConfigNode {}
final class FloatNode extends ConfigNode {}
final class ScalarNode extends ConfigNode {}
final class BoolNode extends ConfigNode {}
function configTree(string $rootName, DictNode $dictNode): TreeBuilder {
$treeBuilder = new TreeBuilder();
configureNode($treeBuilder->root($rootName), $dictNode);
return $treeBuilder;
}
function configureNode($node, ConfigNode $configNode): void {
if ($configNode instanceof DictNode) {
if ($node instanceof ArrayNodeDefinition) {
$resNode = $node;
} else if ($node instanceof NodeBuilder) {
$resNode = $node->arrayNode($configNode->name());
} else {
throw new \RuntimeException('Unexpected dict node type.');
}
/** @var ArrayNodeDefinition $node */
foreach ($configNode->nodesByName() as $name => $childConfigNode) {
configureNode($resNode->children(), $childConfigNode->withAddedAttributes(['name' => $name]));
}
} else if ($configNode instanceof StringNode || $configNode instanceof ScalarNode) {
/** @var NodeBuilder $node */
$resNode = $node->scalarNode($configNode->name());
} else if ($configNode instanceof IntNode) {
/** @var NodeBuilder $node */
$resNode = $node->integerNode($configNode->name());
} else if ($configNode instanceof FloatNode) {
/** @var NodeBuilder $node */
$resNode = $node->floatNode($configNode->name());
} else if ($configNode instanceof BoolNode) {
/** @var NodeBuilder $node */
$resNode = $node->booleanNode($configNode->name());
} else if ($configNode instanceof ListOfNode) {
if ($node instanceof ArrayNodeDefinition) {
$resNode = $node;
} else if ($node instanceof NodeBuilder) {
$resNode = $node->arrayNode($configNode->name());
} else {
throw new \RuntimeException('Unexpected dict node type.');
}
configureArrayNode($resNode, $configNode->node());
} else {
throw new \RuntimeException('Unhandled node types.');
}
$configNode->optionallyConfigure($resNode);
}
function configureArrayNode(ArrayNodeDefinition $arrayNode, ConfigNode $configNode): void {
if ($configNode instanceof DictNode) {
configureNode($arrayNode->arrayPrototype(), $configNode);
} else if ($configNode instanceof StringNode || $configNode instanceof ScalarNode) {
$arrayNode->scalarPrototype();
} else if ($configNode instanceof IntNode) {
$arrayNode->integerPrototype();
} else if ($configNode instanceof FloatNode) {
$arrayNode->floatPrototype();
} else if ($configNode instanceof BoolNode) {
$arrayNode->booleanPrototype();
} else if ($configNode instanceof ListOfNode) {
configureNode($arrayNode->arrayPrototype(), $configNode);
} else {
throw new \RuntimeException('Unhandled node types.');
}
}
/** @param ConfigNode[] $nodesByName */
function dict(array $nodesByName): DictNode { return new DictNode(['nodesByName' => $nodesByName]); }
function tuple(ConfigNode ...$nodes): DictNode { return dict($nodes); }
function listOf(ConfigNode $node): ListOfNode { return new ListOfNode(['node' => $node]); }
function string(): StringNode { return new StringNode(); }
function int(): IntNode { return new IntNode(); }
function float(): FloatNode { return new FloatNode(); }
function scalar(): ScalarNode { return new ScalarNode(); }
function bool(): BoolNode { return new BoolNode(); }
Related Merge Requests
- [RFC] Deprecate end() in config fluent interface #26351 It seems this was an attempt to reduce some of the syntactic noise with defining configuration trees.