Symfony2 Config Component: Config Definition and Processing
Matthias Noback
My previous post was about finding and loading configuration files. I now continue my Symfony2 Config Component quest with a post about the way you can “semantically expose your configuration” (using the TreeBuilder). I wrote this piece to later contribute it to the Symfony documentation so feedback is very welcome!
Validate configuration values
After loading configuration values from all kinds of resources, the values and their structure can be validated using the Definition
part of the Symfony2 Config Component. Configuration values are usually expected to show some kind of hierarchy. Also, values should be of a certain type, be restricted in number or be one of a given set of values. For example, the following configuration (in Yaml) shows a clear hierarchy and some validation rules that should be applied to it (like: “the value for ‘auto_connect’ must be a boolean”):
auto_connect: true
default_connection: mysql
connections:
mysql:
host: localhost
driver: mysql
username: mysql_user
password: j8dsf7sdnk3w89732df9dfn3
sqlite:
host: localhost
driver: sqlite
memory: true
username: sqlite_user
password: 9d732n32ffn3287dsfskfhk
When loading multiple configuration files, it should be possible to merge and overwrite some values. Other values may not be merged and stay as they are in the first file. Also, some keys are only available, when another key has a specific value (in the sample configuration above: the “memory” key only makes sense when the “driver” key is “sqlite”).
TreeBuilder: define a hierarchy of configuration values
All the rules concerning configuration values can be defined using the TreeBuilder.
A TreeBuilder
instance should be returned from a custom Configuration
class which implements the ConfigurationInterface:
namespace Acme\DatabaseConfiguration;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class DatabaseConfiguration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('database');
// add node definitions to the root of the tree
return $treeBuilder;
}
}
Add node definitions to the tree
Variable nodes
A tree contains node definitions which can be layed out in a semantic way. This means, using indentation and the fluent notation, it is possible to reflect the real structure of the configuration values:
$rootNode
->children()
->booleanNode('auto_connect')
->defaultTrue()
->end()
->scalarNode('default_connection')
->defaultValue('default')
->end()
->end()
;
The root node itself is an array node, and has children, like the boolean node “auto_connect” and the scalar node “default_connection”. In general: after defining a node, a call to end()
takes one step up in the hierarchy.
Array nodes
It is possible to add a deeper level to the hierarchy, by adding an array node. The array node itself, may have a pre-defined set of variable nodes:
$rootNode
->arrayNode('connection')
->scalarNode('driver')->end()
->scalarNode('host')->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->end()
;
Or you may define a prototype for each node inside an array node:
$rootNode
->arrayNode('connections')
->prototype('array)
->children()
->scalarNode('driver')->end()
->scalarNode('host')->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->end()
->end()
->end()
;
Array node options
Before defining the children of an array node, you can provide options like:
-
useAttributeAsKey()
: provide the name of a childnode, whose value should be used as the key in the resulting array -
requiresAtLeastOneElement()
: there should be at least one element in the array (works only whenisRequired()
is also called).
An example of this:
$rootNode
->arrayNode('parameters')
->isRequired()
->requiresAtLeastOneElement()
->prototype('array')
->useAttributeAsKey('name')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('value')->isRequired()->end()
->end()
->end()
->end()
;
Default and required values
For all node types, it is possible to define default values and replacement values in case a node has a certain value:
-
defaultValue()
: set a default value -
isRequired()
: must be defined (but may be empty) -
cannotBeEmpty()
: may not contain an empty value -
default*()
(Null, True, False): shortcut fordefaultValue()
-
treat*Like()
(Null, True, False): provide a replacement value in case the value is *
$rootNode
->arrayNode('connection')
->children()
->scalarNode('driver')
->isRequired()
->cannotBeEmpty()
->end()
->scalarNode('host')
->defaultValue('localhost')
->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->booleanNode('memory')
->defaultFalse()
->end()
->end()
->end()
;
Merging options
Extra options concerning the merge process may be provided. For arrays:
performNoDeepMerging()
: when the value is also defined in a second configuration array, don’t try to merge an array, but overwrite it entirely
For all nodes:
cannotBeOverwritten()
: don’t let other configuration arrays overwrite an existing value for this node
Validation rules
More advanced validation rules can be provided using the ExprBuilder. This builder implements a fluent interface for a well-known control structure. The builder is used for adding advanced validation rules to node definitions, like:
$rootNode
->arrayNode('connection')
->children()
->scalarNode('driver')
->isRequired()
->validate()
->ifNotInArray(array('mysql', 'sqlite', 'mssql'))
->thenInvalid('Invalid database driver "%s"')
->end()
->end()
->end()
->end()
A validation rule always has an “if” part. You can specify this part in the following ways:
-
ifTrue()
-
ifString()
-
ifNull()
-
ifArray()
-
ifInArray()
-
ifNotInArray()
A validation rule also requires a “then” part:
-
then()
-
thenEmptyArray()
-
thenInvalid()
-
thenUnset()
The only exception is of course:
always()
Usually, “then” is a closure. It’s return value will be used as a new value for the node, instead of the node’s original value.
Processing configuration values
The Processor
uses the tree as it was built using the TreeBuilder
to process multiple arrays of configuration values that should be merged. If any value is not of the expected type, is mandatory and yet undefined, or could not be validated in some other way, an exception will be thrown. Otherwise the result is a clean array of configuration values.
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Config\Definition\Processor;
use Acme\DatabaseConfiguration;
$config1 = Yaml::parse(__DIR__.'/src/Matthias/config/config.yml');
$config2 = Yaml::parse(__DIR__.'/src/Matthias/config/config_extra.yml');
$configs = array($config1, $config2);
$processor = new Processor;
$configuration = new DatabaseConfiguration;
$processedConfiguration = $processor->processConfiguration($configuration, $configs);