8000 [RFC][Config] Declarative Syntax for Config Tree Builder · Issue #35127 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content
[RFC][Config] Declarative Syntax for Config Tree Builder #35127
Closed
@ragboyjr

Description

@ragboyjr

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    ConfigRFCRFC = Request For Comments (proposals about features that you want to be discussed)Stalled

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0