Validating, normalizing and documenting bundle configuration
The Symfony Configuration Tree Builder is the tool a bundle uses to define which configuration is allowed, which values are mandatory and how invalid input is caught. This article walks through node types, default values, normalization and custom validation rules using a complete example.
Table of contents
- 1. Why the Configuration Tree Builder exists
- 2. ConfigurationInterface as the contract
- 3. Node types: scalar, array, enum and more
- 4. Default values and optional nodes
- 5. Custom validation rules with validate()
- 6. Normalization: unifying alternative notations
- 7. Processing configuration in the extension class
- 8. Error messages developers actually understand
- 9. Node types compared: which one, when
- 10. Summary
- 11. FAQ
1. Why the Configuration Tree Builder exists
As soon as a Symfony bundle needs more than a handful of fixed settings, a plain array of raw values is no longer enough. The Symfony Configuration Tree Builder solves exactly this problem: it describes in PHP code which configuration keys a bundle accepts, what type each value must have, which values are optional, and which combinations are invalid. Without this layer, typos in YAML configuration would only surface at runtime as a cryptic error, often far away from the actual cause.
The Configuration Tree Builder works with the class Symfony\Component\Config\Definition\Builder\TreeBuilder and ultimately produces a tree of node objects that get validated against the actually supplied configuration. This check happens before the extension class sets any container parameters at all, so invalid configuration is rejected immediately with a descriptive error message, instead of surfacing later as a runtime error inside a service.
For bundle authors the Configuration Tree Builder mainly means one thing: the public configuration interface of the bundle is documented explicitly, instead of having to be reverse engineered from the extension class source code. Anyone using bin/console config:dump-reference sees documentation generated automatically straight from the Configuration Tree Builder, including all default values and allowed options.
2. ConfigurationInterface as the contract
A configuration class implements Symfony\Component\Config\Definition\ConfigurationInterface with exactly one method: getConfigTreeBuilder(). This method returns a TreeBuilder instance, on which the entire configuration tree is built through a fluent API. The root node carries the name under which the configuration appears in the target project, such as acme_audit for an audit log bundle, and must match the alias returned by the extension class.
The separation between the configuration class and the extension class is deliberate: the configuration class only describes structure and validation rules, the extension class processes the validated values and turns them into container definitions. This separation makes it possible to test the configuration class in isolation, without building a full container, which considerably reduces the testing effort for the Configuration Tree Builder.
// src/DependencyInjection/Configuration.php
declare(strict_types=1);
namespace Acme\AuditBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* Defines the full configuration tree accepted under the acme_audit key.
*/
final class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('acme_audit');
$rootNode = $treeBuilder->getRootNode();
$rootNode
->children()
->scalarNode('table_name')
->defaultValue('audit_log')
->cannotBeEmpty()
->end()
->integerNode('retention_days')
->min(1)
->defaultValue(90)
->end()
->end();
return $treeBuilder;
}
}
3. Node types: scalar, array, enum and more
The Symfony Configuration Tree Builder offers a matching node type for every common PHP data type. scalarNode accepts any scalar value, while booleanNode, integerNode and floatNode check against the respective type and throw an error immediately on mismatch. enumNode restricts a value to a fixed list of allowed options, which is particularly useful for mode switches such as storage_driver with the values database, redis or file, because a typo is caught immediately instead of loading an unknown driver at runtime.
For more complex structures there is arrayNode with prototype(), to describe lists of similarly shaped entries, for example a list of event types that should be logged. variableNode accepts any value without type checking and is the last resort for cases where the structure is too dynamic for a fixed type. The Configuration Tree Builder also supports nested arrayNode blocks, so entire subtrees such as a notifications section with its own options can be grouped cleanly.
$rootNode
->children()
->enumNode('storage_driver')
->values(['database', 'redis', 'file'])
->defaultValue('database')
->end()
->arrayNode('tracked_events')
->scalarPrototype()->end()
->defaultValue(['user.login', 'user.password_changed', 'order.placed'])
->end()
->arrayNode('notifications')
->addDefaultsIfNotSet()
->children()
->booleanNode('enabled')->defaultTrue()->end()
->scalarNode('channel')->defaultValue('slack')->end()
->end()
->end()
->end();
4. Default values and optional nodes
A core principle of good bundle configuration: every node should have a sensible default value, so a bundle works immediately without any explicit configuration in the target project. defaultValue() sets a concrete value, defaultTrue() and defaultFalse() are convenient shortcuts for boolean nodes. isRequired() instead marks a node as mandatory, forcing it to be set explicitly, which makes sense for security relevant options such as an API key that can never have a sensible generic default.
addDefaultsIfNotSet() on an arrayNode ensures that the entire substructure exists with its defaults, even if the parent key was not specified at all in the target project. Without this method a missing notifications block in the configuration would cause the whole subtree to be absent, instead of appearing with its default values. This distinction is one of the most common pitfalls when first working with the Configuration Tree Builder.
5. Custom validation rules with validate()
Beyond plain type checking, the Symfony Configuration Tree Builder allows custom validation logic through the validate() method available on every node. This lets you express rules that cannot be expressed by a simple type, for example that retention_days must not be smaller than a simultaneously configured minimum_retention_days, or that a storage_driver of redis strictly requires an additional redis_dsn value.
The combination of ifTrue() and then() models conditional validation: ifTrue() defines a check condition as a closure, then() throws an InvalidConfigurationException with a precise error message if the condition holds. This validation runs entirely at configuration time, that is during cache warmup or the first bootstrap, not only when the misconfigured service is actually used.
$rootNode
->children()
->enumNode('storage_driver')
->values(['database', 'redis', 'file'])
->defaultValue('database')
->end()
->scalarNode('redis_dsn')->defaultNull()->end()
->end()
->validate()
->ifTrue(fn (array $v): bool => $v['storage_driver'] === 'redis' && empty($v['redis_dsn']))
->thenInvalid('redis_dsn must be set when storage_driver is "redis".')
->end();
6. Normalization: unifying alternative notations
YAML configuration frequently ends up with different notations for the same thing, such as hyphenated instead of underscored keys, or a comma separated string instead of a YAML list. The Configuration Tree Builder addresses this with the before() method for normalization: it transforms a raw value before the actual type check kicks in, making several input forms possible for the same configuration key without complicating the validation logic.
A typical example is automatically turning a single string into a one element array when a user writes tracked_events: user.login instead of a YAML list. Without normalization this would fail the arrayNode type check, with before() the string is transparently converted into a single element array before further validation runs. This flexibility makes bundle configuration more pleasant without giving up validation strictness.
$rootNode
->children()
->arrayNode('tracked_events')
->beforeNormalization()
->ifString()
->then(fn (string $v): array => [$v])
->end()
->scalarPrototype()->end()
->end()
->end();
7. Processing configuration in the extension class
After validation by the Configuration Tree Builder, the configuration is available inside the extension class as a plain, type safe PHP array. The processConfiguration() method from Symfony\Component\Config\Definition\Processor merges several configuration sources, for example when the same bundle is configured across multiple config/packages files for different environments, applying the rules of the Configuration Tree Builder consistently to all sources.
From the validated array, the extension class then sets container parameters that the bundle's services reference through constructor injection. This step is the reason the Configuration Tree Builder exists in the first place: it guarantees that by the time parameters are produced, every value already has the expected type and every mandatory field is set, so the extension class itself no longer needs defensive error handling.
8. Error messages developers actually understand
An often underestimated benefit of the Symfony Configuration Tree Builder is the quality of its error messages. Instead of a generic type error somewhere deep in the container compilation process, a developer gets an InvalidConfigurationException that names the exact affected configuration key, the expected type and the actually supplied value. info() calls on individual nodes further enrich these error messages with human readable descriptions, which also show up in the automatically generated reference documentation.
# Example output when the Configuration Tree Builder rejects invalid input
$ bin/console cache:warmup
[Symfony\Component\Config\Definition\Exception\InvalidConfigurationException]
Invalid configuration for path "acme_audit.retention_days": Value must be
a positive integer, "-5" given at config/packages/acme_audit.yaml.
[Symfony\Component\Config\Definition\Exception\InvalidConfigurationException]
redis_dsn must be set when storage_driver is "redis".
Such messages appear as early as the cache:warmup command, well before an affected service is even instantiated. That saves debugging a detour through stack traces from deeply nested service constructors, and makes the Configuration Tree Builder one of the most effective tools against configuration errors in production. Anyone writing custom validation rules with thenInvalid() should phrase the message so it immediately names the required fix, instead of only describing the error state. A message like redis_dsn must be set when storage_driver is redis is far more helpful than a plain Invalid configuration, because it tells the developer exactly which key to add, without having to search the configuration class source code.
9. Node types compared: which one, when
Choosing the right node type in the Symfony Configuration Tree Builder determines how early invalid configuration gets caught. The table below maps common node types to their typical use cases.
| Node type | Use case | Type checking | Example |
|---|---|---|---|
| scalarNode | Free text, IDs, names | No type restriction | table_name |
| enumNode | Fixed set of modes | Strict against a value list | storage_driver |
| integerNode | Counts, durations | Strictly integer, min/max | retention_days |
| arrayNode + prototype | Lists of similarly shaped entries | Every element type checked | tracked_events |
| variableNode | Arbitrary, dynamic structure | None, last resort only | extra_metadata |
Principle: only use variableNode when no fixed structure can genuinely be described. Every other case benefits from a more specific node type, because the Configuration Tree Builder then catches errors already at configuration time, instead of letting them surface later as a runtime error inside a service.
Mironsoft
Symfony bundle development with robust, validated configuration
Configuration errors surfacing at runtime instead of at deploy time?
We build configuration trees for your internal Symfony bundles, with clear default values, custom validation rules and error messages your team understands without reading the source code.
Configuration design
Configuration keys, defaults and validation rules for your bundles
Migrating existing bundles
Moving raw array configuration onto a clean Configuration Tree Builder
Documentation
Automatically generated configuration reference for your development team
10. Summary
The Symfony Configuration Tree Builder is the layer between raw YAML or PHP configuration and the type safe values an extension class processes. Through ConfigurationInterface, a dedicated class describes the entire configuration tree: node types such as scalarNode, enumNode and arrayNode define the allowed type, default values make a bundle immediately usable, and custom validation rules via validate() catch combinations that cannot be expressed by a simple type.
Normalization with before() enables pleasant, flexible input forms without losing validation strictness. Anyone who consistently uses the Configuration Tree Builder, instead of reading configuration unchecked from a raw array, gets precise error messages at configuration time, automatically generated documentation, and a bundle that can safely be used in unfamiliar projects without typos only surfacing in production.
Symfony Configuration Tree Builder — At a glance
Foundation
ConfigurationInterface with getConfigTreeBuilder(), root node named like the extension alias.
Node types
scalarNode, enumNode, integerNode, arrayNode with prototype depending on the expected type.
Validation
validate() with ifTrue()/thenInvalid() for rules beyond the plain type system.
Normalization
beforeNormalization() unifies alternative notations before the type check.