How to expose a Semantic Configuration for a Bundle

If you open your application configuration file (usually app/config/config.yml), you’ll see a number of different configuration “namespaces”, such as framework, twig, and doctrine. Each of these configures a specific bundle, allowing you to configure things at a high level and then let the bundle make all the low-level, complex changes that result.

For example, the following tells the FrameworkBundle to enable the form integration, which involves the defining of quite a few services as well as integration of other related components:

  • YAML
    framework:
        # ...
        form:            true
    
  • XML
    <framework:config>
        <framework:form />
    </framework:config>
    
  • PHP
    $container->loadFromExtension('framework', array(
        // ...
        'form'            => true,
        // ...
    ));
    

When you create a bundle, you have two choices on how to handle configuration:

  1. Normal Service Configuration (easy):

    You can specify your services in a configuration file (e.g. services.yml) that lives in your bundle and then import it from your main application configuration. This is really easy, quick and totally effective. If you make use of parameters, then you still have the flexibility to customize your bundle from your application configuration. See “Importing Configuration with imports” for more details.

  2. Exposing Semantic Configuration (advanced):

    This is the way configuration is done with the core bundles (as described above). The basic idea is that, instead of having the user override individual parameters, you let the user configure just a few, specifically created options. As the bundle developer, you then parse through that configuration and load services inside an “Extension” class. With this method, you won’t need to import any configuration resources from your main application configuration: the Extension class can handle all of this.

The second option - which you’ll learn about in this article - is much more flexible, but also requires more time to setup. If you’re wondering which method you should use, it’s probably a good idea to start with method #1, and then change to #2 later if you need to.

The second method has several specific advantages:

  • Much more powerful than simply defining parameters: a specific option value might trigger the creation of many service definitions;
  • Ability to have configuration hierarchy
  • Smart merging when several configuration files (e.g. config_dev.yml and config.yml) override each other’s configuration;
  • Configuration validation (if you use a Configuration Class);
  • IDE auto-completion when you create an XSD and developers use XML.

Creating an Extension Class

If you do choose to expose a semantic configuration for your bundle, you’ll first need to create a new “Extension” class, which will handle the process. This class should live in the DependencyInjection directory of your bundle and its name should be constructed by replacing the Bundle suffix of the Bundle class name with Extension. For example, the Extension class of AcmeHelloBundle would be called AcmeHelloExtension:

// Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AcmeHelloExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        // where all of the heavy logic is done
    }

    public function getXsdValidationBasePath()
    {
        return __DIR__.'/../Resources/config/';
    }

    public function getNamespace()
    {
        return 'http://www.example.com/symfony/schema/';
    }
}

Note

The getXsdValidationBasePath and getNamespace methods are only required if the bundle provides optional XSD’s for the configuration.

The presence of the previous class means that you can now define an acme_hello configuration namespace in any configuration file. The namespace acme_hello is constructed from the extension’s class name by removing the word Extension and then lowercasing and underscoring the rest of the name. In other words, AcmeHelloExtension becomes acme_hello.

You can begin specifying configuration under this namespace immediately:

  • YAML
    # app/config/config.yml
    acme_hello: ~
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" ?>
    
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:acme_hello="http://www.example.com/symfony/schema/"
        xsi:schemaLocation="http://www.example.com/symfony/schema/ http://www.example.com/symfony/schema/hello-1.0.xsd">
    
       <acme_hello:config />
       ...
    
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('acme_hello', array());
    

Tip

If you follow the naming conventions laid out above, then the load() method of your extension code is always called as long as your bundle is registered in the Kernel. In other words, even if the user does not provide any configuration (i.e. the acme_hello entry doesn’t even appear), the load() method will be called and passed an empty $configs array. You can still provide some sensible defaults for your bundle if you want.

Parsing the $configs Array

Whenever a user includes the acme_hello namespace in a configuration file, the configuration under it is added to an array of configurations and passed to the load() method of your extension (Symfony2 automatically converts XML and YAML to an array).

Take the following configuration:

  • YAML
    # app/config/config.yml
    acme_hello:
        foo: fooValue
        bar: barValue
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" ?>
    
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:acme_hello="http://www.example.com/symfony/schema/"
        xsi:schemaLocation="http://www.example.com/symfony/schema/ http://www.example.com/symfony/schema/hello-1.0.xsd">
    
        <acme_hello:config foo="fooValue">
            <acme_hello:bar>barValue</acme_hello:bar>
        </acme_hello:config>
    
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('acme_hello', array(
        'foo' => 'fooValue',
        'bar' => 'barValue',
    ));
    

The array passed to your load() method will look like this:

array(
    array(
        'foo' => 'fooValue',
        'bar' => 'barValue',
    )
)

Notice that this is an array of arrays, not just a single flat array of the configuration values. This is intentional. For example, if acme_hello appears in another configuration file - say config_dev.yml - with different values beneath it, then the incoming array might look like this:

array(
    array(
        'foo' => 'fooValue',
        'bar' => 'barValue',
    ),
    array(
        'foo' => 'fooDevValue',
        'baz' => 'newConfigEntry',
    ),
)

The order of the two arrays depends on which one is set first.

It’s your job, then, to decide how these configurations should be merged together. You might, for example, have later values override previous values or somehow merge them together.

Later, in the Configuration Class section, you’ll learn of a truly robust way to handle this. But for now, you might just merge them manually:

public function load(array $configs, ContainerBuilder $container)
{
    $config = array();
    foreach ($configs as $subConfig) {
        $config = array_merge($config, $subConfig);
    }

    // now use the flat $config array
}

Caution

Make sure the above merging technique makes sense for your bundle. This is just an example, and you should be careful to not use it blindly.

Using the load() Method

Within load(), the $container variable refers to a container that only knows about this namespace configuration (i.e. it doesn’t contain service information loaded from other bundles). The goal of the load() method is to manipulate the container, adding and configuring any methods or services needed by your bundle.

Loading External Configuration Resources

One common thing to do is to load an external configuration file that may contain the bulk of the services needed by your bundle. For example, suppose you have a services.xml file that holds much of your bundle’s service configuration:

use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\Config\FileLocator;

public function load(array $configs, ContainerBuilder $container)
{
    // prepare your $config variable

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

You might even do this conditionally, based on one of the configuration values. For example, suppose you only want to load a set of services if an enabled option is passed and set to true:

public function load(array $configs, ContainerBuilder $container)
{
    // prepare your $config variable

    $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));

    if (isset($config['enabled']) && $config['enabled']) {
        $loader->load('services.xml');
    }
}

Configuring Services and Setting Parameters

Once you’ve loaded some service configuration, you may need to modify the configuration based on some of the input values. For example, suppose you have a service whose first argument is some string “type” that it will use internally. You’d like this to be easily configured by the bundle user, so in your service configuration file (e.g. services.xml), you define this service and use a blank parameter - acme_hello.my_service_type - as its first argument:

<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="acme_hello.my_service_type" />
    </parameters>

    <services>
        <service id="acme_hello.my_service" class="Acme\HelloBundle\MyService">
            <argument>%acme_hello.my_service_type%</argument>
        </service>
    </services>
</container>

But why would you define an empty parameter and then pass it to your service? The answer is that you’ll set this parameter in your extension class, based on the incoming configuration values. Suppose, for example, that you want to allow the user to define this type option under a key called my_type. Add the following to the load() method to do this:

public function load(array $configs, ContainerBuilder $container)
{
    // prepare your $config variable

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

    if (!isset($config['my_type'])) {
        throw new \InvalidArgumentException('The "my_type" option must be set');
    }

    $container->setParameter('acme_hello.my_service_type', $config['my_type']);
}

Now, the user can effectively configure the service by specifying the my_type configuration value:

  • YAML
    # app/config/config.yml
    acme_hello:
        my_type: foo
        # ...
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" ?>
    
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:acme_hello="http://www.example.com/symfony/schema/"
        xsi:schemaLocation="http://www.example.com/symfony/schema/ http://www.example.com/symfony/schema/hello-1.0.xsd">
    
        <acme_hello:config my_type="foo">
            <!-- ... -->
        </acme_hello:config>
    
    </container>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('acme_hello', array(
        'my_type' => 'foo',
        // ...
    ));
    

Global Parameters

When you’re configuring the container, be aware that you have the following global parameters available to use:

  • kernel.name
  • kernel.environment
  • kernel.debug
  • kernel.root_dir
  • kernel.cache_dir
  • kernel.logs_dir
  • kernel.bundle_dirs
  • kernel.bundles
  • kernel.charset

Caution

All parameter and service names starting with a _ are reserved for the framework, and new ones must not be defined by bundles.

Validation and Merging with a Configuration Class

So far, you’ve done the merging of your configuration arrays by hand and are checking for the presence of config values manually using the isset() PHP function. An optional Configuration system is also available which can help with merging, validation, default values, and format normalization.

Note

Format normalization refers to the fact that certain formats - largely XML - result in slightly different configuration arrays and that these arrays need to be “normalized” to match everything else.

To take advantage of this system, you’ll create a Configuration class and build a tree that defines your configuration in that class:

// src/Acme/HelloBundle/DependencyExtension/Configuration.php
namespace Acme\HelloBundle\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_hello');

        $rootNode
            ->children()
                ->scalarNode('my_type')->defaultValue('bar')->end()
            ->end()
        ;

        return $treeBuilder;
    }

This is a very simple example, but you can now use this class in your load() method to merge your configuration and force validation. If any options other than my_type are passed, the user will be notified with an exception that an unsupported option was passed:

use Symfony\Component\Config\Definition\Processor;
// ...

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

    // ...
}

The processConfiguration() method uses the configuration tree you’ve defined in the Configuration class to validate, normalize and merge all of the configuration arrays together.

The Configuration class can be much more complicated than shown here, supporting array nodes, “prototype” nodes, advanced validation, XML-specific normalization and advanced merging. The best way to see this in action is to checkout out some of the core Configuration classes, such as the one from the FrameworkBundle Configuration or the TwigBundle Configuration.

Default Configuration Dump

New in version 2.1: The config:dump-reference command was added in Symfony 2.1

The config:dump-reference command allows a bundle’s default configuration to be output to the console in yaml.

As long as your bundle’s configuration is located in the standard location (YourBundle\DependencyInjection\Configuration) and does not have a __constructor() it will work automatically. If you have a something different your Extension class will have to override the Extension::getConfiguration() method. Have it return an instance of your Configuration.

Comments and examples can be added to your configuration nodes using the ->setInfo() and ->setExample() methods:

// src/Acme/HelloBundle/DependencyExtension/Configuration.php
namespace Acme\HelloBundle\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_hello');

        $rootNode
            ->children()
                ->scalarNode('my_type')
                    ->defaultValue('bar')
                    ->setInfo('what my_type configures')
                    ->setExample('example setting')
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }

This text appears as yaml comments in the output of the config:dump-reference command.

Extension Conventions

When creating an extension, follow these simple conventions:

  • The extension must be stored in the DependencyInjection sub-namespace;
  • The extension must be named after the bundle name and suffixed with Extension (AcmeHelloExtension for AcmeHelloBundle);
  • The extension should provide an XSD schema.

If you follow these simple conventions, your extensions will be registered automatically by Symfony2. If not, override the Bundle :method:`Symfony\\Component\\HttpKernel\\Bundle\\Bundle::build` method in your bundle:

use Acme\HelloBundle\DependencyInjection\UnconventionalExtensionClass;

class AcmeHelloBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        // register extensions that do not follow the conventions manually
        $container->registerExtension(new UnconventionalExtensionClass());
    }
}

In this case, the extension class must also implement a getAlias() method and return a unique alias named after the bundle (e.g. acme_hello). This is required because the class name doesn’t follow the standards by ending in Extension.

Additionally, the load() method of your extension will only be called if the user specifies the acme_hello alias in at least one configuration file. Once again, this is because the Extension class doesn’t follow the standards set out above, so nothing happens automatically.