After writing this article I've started to write more documentation for the Config component. First I published it here, then it became the official documentation of the Config component.

After reading the Cookbook article How to expose a Semantic Configuration for a Bundle I became interested in the possibilities for defining a configuration tree using the TreeBuilder from the Symfony component Config. ThisTreeBuilderallows you to build a schema against which the configuration values (or short: config) from/app/config.yml` and the like are to be validated.

Create an Extension

First make sure that your bundle has a /DependencyInjection/[BundleNameWithoutBundle]Extension.php containing the extension class:

namespace Acme\DemoBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Definition\Processor;

class AcmeDemoExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $processor = new Processor();
        $configuration = new Configuration();
        $config = $processor->processConfiguration($configuration, $configs);

        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');
    }

    public function getAlias()
    {
        return 'acme_demo';
    }
}

This enables you to use the key "acme_demo" in the /app/config.yml file, like this:

acme_demo:
    enabled: true

But right now, this would cause an exception to be thrown, which says that the "enabled" key is not defined as a config value.

The Configuration class

To make the "enabled" key valid, we have to make sure the file /DependencyInjection/Configuration.php exists in your bundle. This file contains the Configuration class for your bundle and it's method getConfigTreeBuilder, which should return the TreeBuilder object that describes the structure of your config values:

namespace Acme\DemoBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('acme_demo');

        return $rootNode;
    }
}

BooleanNode

There are many ways to specify validation rules for configuration values, but first, let's create a fresh node in the tree for our still invalid value "enabled". The value should always be of type "boolean", so before we return $rootNode, we add:

$rootNode->
    children()
        ->booleanNode('enabled')->end()
    end()
;

As you can see, the TreeBuilder and the nodes all implement a fluent interface, so we can write the tree down in a semantic way: the way it looks reflects the way it is.

The calls to end() mean that we want to move up again to the parent node of the current node.

ScalarNode

Let's add a node for scalar values (like strings, numbers, etc.):

$rootNode->
    children()
        ->booleanNode('enabled')->end()
        ->scalarNode('default_user')
            ->isRequired()
            ->cannotBeEmpty()
        ->end()
    end()
;

The new node defines a config value "default_user" which is required and may not be empty.

ArrayNode

Now, let's add an array node, which allows for an array of users to be defined in the config file:

$rootNode->
    children()
        ->booleanNode('enabled')->end()
        ->scalarNode('default_user')
            ->isRequired()
            ->cannotBeEmpty()
        ->end()
        ->arrayNode('users')
            ->requiresAtLeastOneElement()
            ->prototype('array')
                ->children()
                    ->scalarNode('full_name')
                        ->isRequired(true)
                    ->end()
                    ->booleanNode('is_active')
                        ->defaultValue(true)
                    ->end()
                ->end()
            ->end()
        ->end()
    ->end()
;

A few other things are shown here: the config value "user" contains multiple subnodes, of which the prototype is an array node. The children of these array nodes themselves will be a scalar node called "full_name" and a boolean node called "is_active", of which the default value is true. An extra requirement is that at least one such user should be defined.

Before normalization - then what?

The final thing I want to show is how to change the overall structure of the config values, before they are validated. This would allow you to add shortcuts, or special ways of handling certain config structures (see the DoctrineBundle Configuration class for a beautiful example of this: the only connection defined will be moved to the "connections" section, before processing). In my example, I remove all the user definitions, when the value of "enabled" is false or is not set:

$rootNode->
    ->beforeNormalization()
        ->ifTrue(function($v) {
            // $v contains the raw configuration values
            return !isset($v['enabled']) || false === $v['enabled'];
        })
        ->then(function($v) {
            unset($v['users']);
            return $v;
        })
        ->end()
    ->children()
        // ...
    ->end()
;

You can see here that we first check (using ifTrue()) if the value of "enabled" is false or is not set at all. If this is the case, we unset the "users" key in the array containing the raw config values.

A setting like "enabled" may also be very important for deciding whether you should load the services of your bundle or not. An example usage of this would be to make the loading of your services.xml file conditional in /DependencyInjection/AcmeDemoBundleExtension.php:

class AcmeDemoExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $processor = new Processor();
        $configuration = new Configuration();
        $config = $processor->processConfiguration($configuration, $configs);

        if (isset($config['enabled']) && $config['enabled']) {
            $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
            $loader->load('services.xml');
        }
    }
}
PHP Symfony2 dependency injection service container configuration