Symfony2 Config Component: Config Definition and Processing

Posted on by Matthias Noback

Please note: I have rewritten this post for the official Symfony Config Component documentation. The version on the Symfony website will be kept up to date, while this one will not.

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 when isRequired() 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 for defaultValue()

  • 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);
PHP Symfony2 configuration validation documentation
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).

Hi Matthias,

My config data is below:

checkType: url
params:
startUrl:
route: start_route_name
slugs:
step: intro
endUrl:
route: end_route_name

checkType: another_check
params:

How can I define a rule that if checkType is 'url' then params is required and must be an array of startUrl and endUrl? if checkType is 'another_check', params is not required.

I tried:
->scalarNode('checkType')
->isRequired()
->validate()
->ifInArray(array('url'))
but I did not know how to write the then() part

Thanks for your guidance.

Sergii

Hi! Thnx for your topic.

My task is to load config of swiftmailer dynamicly (depends of Host). I have several configs for it in app/config/config.yml. I tried to set right config in Bundle/DependencyInjection/BundleExtension.php in method "load" like this:

$container->setParameter( 'mailer_transport', 'gmail' );
$container->setParameter( 'mailer_user', 'example@example.com' );
$container->setParameter( 'mailer_password', 'pass' );

Its good when I run it first time but than method "load" falls in cache.

Maybe you can help me. In what way should I go to fix this. Or maybe I should create dynamicly Swift_Transport class and set it to Swift_Mailer instead?

Thank you!

Matthias Noback

Since the host is variable per request, and the application configuration is fixed for any request, I think it would be a good idea to also define transport details dynamically. You could for instance create a transport factory which takes the request as a dependency.

Sergii

How do you think, its a good idea or not ?

Sergii

Thnx a lot.
Already fix it by creating custom Transport class and extend it from original Transport class. Made my class like service and now I can set data in __construct of my class.
class SatellitesTransport extends \Swift_Transport_EsmtpTransport.
public function __construct(\Swift_Transport_IoBuffer $buf, array $extensionHandlers, \Swift_Events_EventDispatcher $dispatcher, Satellites $satellites)
{
parent::__construct($buf, $extensionHandlers, $dispatcher);

$this->setHost( $satellites->get('email', 'host') );
$this->setPort( $satellites->get('email', 'port') );
$this->setEncryption( $satellites->get('email', 'encryption') );
$this->setAuthMode( $satellites->get('email', 'auth_mode') );
$this->setUsername( $satellites->get('email', 'username') );
$this->setPassword( $satellites->get('email', 'password') );
}

Something like this.

Luciano Mammino

Is there any way to convert values inside the Tree builder?
Suppose i have values that should be represented internally as integer constants, but i prefer to use more declarative strings in configuration. Is there any way to process the strings and map them to integer constants?

Example:

Constants: pear (1), apple (2), banana (3), orange (4)

Configuration (in config.yml)


fruits: [pear, apple, banana, orange]

i want it to be compiled to


array(
'fruits' => array(1, 2, 3, 4),
);

Matthias Noback

Hi Luciano,
You might try using the beforeNormalization() method which lets you modify the original array of values before the date will be normalized. The validation method could then be ifNotInArray().
Good luck,

Luciano Mammino

Thanks a lot Matthias!
I have i slightly different (and a bit more complex) scenario now.
I have an array (prototyped array) and i want to transform a single string value in the array to an integer constant.
Referring to my previous fruitNames/constants example I can provide the following sample code:

things:
paul:
foo: bar
bar: baz
fruit: banana
eric:
baz: bar
foo: foo
fruit: pear
jimmy:
bar: bar
fruit: orange

should be transformed to:

array(
'things' => array(
'paul' => array(
'foor' => 'bar',
'bar' => 'baz',
'fruit' => 3
),
'eric' => array(
'baz' => 'bar',
'foo' => 'foo',
'fruit' => 1
),
'jimmy' => array(
'bar' => 'bar',
'fruit' => 4
)
)
)

I tried some different combination but it turned out that the beforeNormalization is never being execute (i tried to print something both in the if and the then part, but nothing showed up). I am not sure on how the beforeNormalization should be used in the TreeBuilder.
Just to have a reference, I did something like the following code: http://pastie.org/5360346

Luciano Mammino

I spotted the problem and i've been able to make it work.
My real use case adopted default values. I was somewhat convinced that the beforeNormalization would also been called on unset values using the provided default value as parameter. That's not the case: that method is called only if you set explicit values. So i had to provide an already normalized default value and everything started working fine.
Thanks again Matthias!

Matthias Noback

Ah, well, I have also experienced these kind of surprises many times when working with the TreeBuilder. Glad it works for you now! Enjoy,

Ross Cousens

Hi,

I have a quick query regarding simple configuration definitions. I found your page (and the corresponding cookbook article) very informative, but what it fails to do (for me at least) is connect the config treeBuilder definitions to a corresponding config annotation. As such, I am confused about one thing:

I have a bundle config in my app/config/config.yml as follows:


ivs_signup:
service_parameters:
microsoft:
client_id: 000000xxxxx
client_secret: AsCvR329ldUxxxxxxxxxxxxxxx
auth_uri: https://login.live.com/oauth20_authorize.srf
token_uri: https://login.live.com/oauth20_token.srf
redirect_uri: http://regsrv.localdomain/app_dev.php/services/callback/microsoft

My treeBuilder is as follows:

$rootNode
->children()
->arrayNode('service_parameters')
->useAttributeAsKey('name')
->prototype('array')
->children()
->scalarNode('client_id')->end()
->scalarNode('client_secret')->end()
->scalarNode('auth_uri')->end()
->scalarNode('token_uri')->end()
->scalarNode('redirect_uri')->end()
->end()
->end()
->end()
->end()
;

2 things:

Why does Symfony not care that the useAttributeAsKey('name') directive under service_parameters does not reference an existing node?

This is my config:debug from console:

ivs_signup:
service_parameters:

# Prototype
name:
client_id: ~
client_secret: ~
auth_uri: ~
token_uri: ~
redirect_uri: ~

If I remove the useAttributeByKey directive, the config:dump information loses both "prototype" and the name: node.

I don't understand if what I'm doing is correct, I just know it works. What I want to achieve is the ability to configure a bunch of service_parameters under named array keys based off the children name of service_parameters.

Cheers!

Ross Cousens

I didn't realise how bad my ability to format text in comments was, I might just try and email you.

Matthias Noback

Hi Ross, no problem, I will try to fix the formatting. Then I will see if I can answer your question.

Igor

Hello! Am I correct understood that useAttributeAsKey it is like array_flip? Thanks!

Matthias Noback

Not really. What happens is, when you have an array like:

[php]
array(
array('name' => 'primary', 'host" => 'localhost', 'driver' => 'mysql'),
array('name' => 'secondary', 'host' => 'localhost', 'driver' => 'mssql'),
);
[/php]

The result of using useAttributeAsKey will be an array like this:

[php]
array(
'primary' => array('host" => 'localhost', 'driver' => 'mysql'),
'secondary' => array('host' => 'localhost', 'driver' => 'mssql'),
);
[/php]

Good luck!

Christophe Willemsen

Hi Matthias,

Thank you very much for this very nice explanation of a Component that is unfortunately not yet good documented.

I have one question, what is the goal of a "prototype" ?

Thank you

Matthias Noback

Thank you!

A prototype can be used to add a definition which may be repeated many times inside the current node. According to the prototype definition in the example above, it is possible to have multiple connection arrays (containing a "driver", "host", etc. key), like:

[php]
array('connections' => array(
array('driver' => '...', 'host' => '...'),
array('driver' => '...', 'host' => '...'),
);
[/php]

Good luck!

Christophe Willemsen

Thanks for your clarification.

Regards,

Christophe