Skip to content

How to Create Friendly Configuration for a Bundle

Edit this page

If you open your main application configuration directory (usually config/packages/), you'll see a number of different files, such as framework.yaml, twig.yaml and doctrine.yaml. Each of these configures a specific bundle, allowing you to define options at a high level and then let the bundle make all the low-level, complex changes based on your settings.

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

1
2
3
# config/packages/framework.yaml
framework:
    form: true

There are two different ways of creating friendly configuration for a bundle:

  1. Using the main bundle class: this is recommended for new bundles and for bundles following the recommended directory structure;
  2. Using the Bundle extension class: this was the traditional way of doing it, but nowadays it's only recommended for bundles following the legacy directory structure.

Using the AbstractBundle Class

In bundles extending the AbstractBundle class, you can add all the logic related to processing the configuration in that class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/AcmeSocialBundle.php
namespace Acme\SocialBundle;

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class AcmeSocialBundle extends AbstractBundle
{
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->rootNode()
            ->children()
                ->arrayNode('twitter')
                    ->children()
                        ->integerNode('client_id')->end()
                        ->scalarNode('client_secret')->end()
                    ->end()
                ->end() // twitter
            ->end()
        ;
    }

    public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
    {
        // the "$config" variable is already merged and processed so you can
        // use it directly to configure the service container (when defining an
        // extension class, you also have to do this merging and processing)
        $container->services()
            ->get('acme_social.twitter_client')
            ->arg(0, $config['twitter']['client_id'])
            ->arg(1, $config['twitter']['client_secret'])
        ;
    }
}

Note

The configure() and loadExtension() methods are called only at compile time.

Tip

The AbstractBundle::configure() method also allows importing the configuration definition from one or more files:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AcmeSocialBundle.php
namespace Acme\SocialBundle;

// ...
class AcmeSocialBundle extends AbstractBundle
{
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->import('../config/definition.php');
        // you can also use glob patterns
        //$definition->import('../config/definition/*.php');
    }

    // ...
}
1
2
3
4
5
6
7
8
9
10
// config/definition.php
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;

return static function (DefinitionConfigurator $definition): void {
    $definition->rootNode()
        ->children()
            ->scalarNode('foo')->defaultValue('bar')->end()
        ->end()
    ;
};

Using the Bundle Extension

This is the traditional way of creating friendly configuration for bundles. For new bundles it's recommended to use the main bundle class, but the traditional way of creating an extension class still works.

Imagine you are creating a new bundle - AcmeSocialBundle - which provides integration with X/Twitter. To make your bundle configurable to the user, you can add some configuration that looks like this:

1
2
3
4
5
# config/packages/acme_social.yaml
acme_social:
    twitter:
        client_id: 123
        client_secret: your_secret

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 correct services and parameters inside an "Extension" class.

Note

The root key of your bundle configuration (acme_social in the previous example) is automatically determined from your bundle name (it's the snake case of the bundle name without the Bundle suffix).

See also

Read more about the extension in How to Load Service Configuration inside a Bundle.

Tip

If a bundle provides an Extension class, then you should not generally override any service container parameters from that bundle. The idea is that if an extension class is present, every setting that should be configurable should be present in the configuration made available by that class. In other words, the extension class defines all the public configuration settings for which backward compatibility will be maintained.

See also

For parameter handling within a dependency injection container see Using Parameters within a Dependency Injection Class.

Processing the $configs Array

First things first, you have to create an extension class as explained in How to Load Service Configuration inside a Bundle.

Whenever a user includes the acme_social key (which is the DI alias) in a configuration file, the configuration under it is added to an array of configurations and passed to the load() method of your extension (Symfony automatically converts the configuration to an array).

For the configuration example in the previous section, the array passed to your load() method will look like this:

1
2
3
4
5
6
7
8
[
    [
        'twitter' => [
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ],
    ],
]

Notice that this is an array of arrays, not just a single flat array of the configuration values. This is intentional, as it allows Symfony to parse several configuration resources. For example, if acme_social appears in another configuration file - say config/packages/dev/acme_social.yaml - with different values beneath it, the incoming array might look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
    // values from config/packages/acme_social.yaml
    [
        'twitter' => [
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ],
    ],
    // values from config/packages/dev/acme_social.yaml
    [
        'twitter' => [
            'client_id' => 456,
        ],
    ],
]

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

But don't worry! Symfony's Config component will help you merge these values, provide defaults and give the user validation errors on bad configuration. Here's how it works. Create a Configuration class in the DependencyInjection directory and build a tree that defines the structure of your bundle's configuration.

The Configuration class to handle the sample configuration looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/DependencyInjection/Configuration.php
namespace Acme\SocialBundle\DependencyInjection;

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

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder('acme_social');

        $treeBuilder->getRootNode()
            ->children()
                ->arrayNode('twitter')
                    ->children()
                        ->integerNode('client_id')->end()
                        ->scalarNode('client_secret')->end()
                    ->end()
                ->end() // twitter
            ->end()
        ;

        return $treeBuilder;
    }
}

Warning

Build the configuration tree from static information only. Don't read container parameters (such as kernel.debug), environment variables or the state of the machine (installed PHP extensions, operating system) while building it: Symfony generates the configuration reference from the tree, so it must be identical in every environment. When a default value depends on such things, leave the node without a default, describe the fallback in info() and resolve it when loading the configuration.

See also

The Configuration class can be much more complicated than shown here, supporting "prototype" nodes, advanced validation, plural/singular normalization and advanced merging. You can read more about this in the Config component documentation. You can also see it in action by checking out some core Configuration classes, such as the one from the FrameworkBundle Configuration or the TwigBundle Configuration.

This class can now be used in your load() method to merge configurations and force validation (e.g. if an additional option was passed, an exception will be thrown):

1
2
3
4
5
6
7
8
9
10
// src/DependencyInjection/AcmeSocialExtension.php
public function load(array $configs, ContainerBuilder $container): void
{
    $configuration = new Configuration();

    $config = $this->processConfiguration($configuration, $configs);

    // you now have these 2 config keys
    // $config['twitter']['client_id'] and $config['twitter']['client_secret']
}

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

Now, you can use the $config variable to modify a service provided by your bundle. For example, imagine your bundle has the following example config:

1
2
3
4
5
6
7
8
9
10
// src/Resources/config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Acme\SocialBundle\TwitterClient;

return function (ContainerConfigurator $container) {
    $container->services()
        ->set('acme_social.twitter_client', TwitterClient::class)
            ->args([abstract_arg('client_id'), abstract_arg('client_secret')]);
};

In your extension, you can load this and dynamically set its arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/DependencyInjection/AcmeSocialExtension.php
namespace Acme\SocialBundle\DependencyInjection;

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

public function load(array $configs, ContainerBuilder $container): void
{
    $loader = new PhpFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
    $loader->load('services.php');

    $configuration = new Configuration();
    $config = $this->processConfiguration($configuration, $configs);

    $definition = $container->getDefinition('acme_social.twitter_client');
    $definition->replaceArgument(0, $config['twitter']['client_id']);
    $definition->replaceArgument(1, $config['twitter']['client_secret']);
}

Tip

Instead of calling processConfiguration() in your extension each time you provide some configuration options, you might want to use the ConfigurableExtension to do this automatically for you:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/DependencyInjection/HelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;

class AcmeHelloExtension extends ConfigurableExtension
{
    // note that this method is called loadInternal and not load
    protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void
    {
        // ...
    }
}

This class uses the getConfiguration() method to get the Configuration instance.

Using the Config component is fully optional. The load() method gets an array of configuration values. You can instead parse these arrays yourself (e.g. by overriding configurations and using isset to check for the existence of a value):

1
2
3
4
5
6
7
8
9
10
public function load(array $configs, ContainerBuilder $container): void
{
    $config = [];
    // let resources override the previous set value
    foreach ($configs as $subConfig) {
        $config = array_merge($config, $subConfig);
    }

    // ... now use the flat $config array
}

Modifying the Configuration of Another Bundle

If you have multiple bundles that depend on each other, it may be useful to allow one Extension class to modify the configuration passed to another bundle's Extension class. This can be achieved using a prepend extension. For more details, see How to Simplify Configuration of Multiple Bundles.

Forwarding a Configuration Section to Another Bundle

8.2

The aliasOf() method was introduced in Symfony 8.2.

When a section of your bundle configuration moves to another bundle, you can keep supporting the old key so existing applications don't break. Use the aliasOf() method to declare that the section belongs to the configuration of the other bundle, passing the root key of that configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/AcmeSocialBundle.php
namespace Acme\SocialBundle;

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class AcmeSocialBundle extends AbstractBundle
{
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->rootNode()
            ->children()
                // this section is now handled by AcmeBlueskyBundle
                ->variableNode('bluesky')->aliasOf('acme_bluesky')
                    // optionally, ask users to move to the new key (the
                    // deprecation is only triggered if the old key is used)
                    ->setDeprecated('acme/social-bundle', '2.1')
                ->end()
            ->end()
        ;
    }
}

Symfony moves the contents of the old key to the configuration of the other bundle, so the following configuration is processed as if it were written under the acme_bluesky key:

1
2
3
4
# config/packages/acme_social.yaml
acme_social:
    bluesky:
        handle: acme.example.com

This happens after all bundles have prepended their configuration and before any of them is loaded, so the configuration that other bundles prepend under the old key is forwarded too.

If an application uses both keys, the values of the old key are merged first, so the values defined under acme_bluesky take precedence.

The alias node normalizes the value before forwarding it, so it can keep accepting a shorthand of the old section. For example, to accept a string as the value of the handle option:

1
2
3
4
5
6
7
8
9
10
11
$definition->rootNode()
    ->children()
        ->variableNode('bluesky')->aliasOf('acme_bluesky')
            // "bluesky: acme.example.com" becomes "bluesky: { handle: ... }"
            ->beforeNormalization()
                ->ifString()
                ->then(static fn (string $handle) => ['handle' => $handle])
            ->end()
        ->end()
    ->end()
;

Keep in mind the following restrictions:

  • only the direct children of the root node can be aliases and they must accept arrays (use a variableNode(), because the other bundle already validates the contents of the section);
  • after normalization, the value must be an array or null (which is forwarded as an empty array);
  • aliases can't be chained, so the alias must point to the bundle that actually handles the section.

Dump the Configuration

The config:dump-reference command dumps the default configuration of a bundle in the console using the Yaml format.

As long as your bundle's configuration is located in the standard location (<YourBundle>/src/DependencyInjection/Configuration) and does not have a constructor, it will work automatically. If you have something different, your Extension class must override the Extension::getConfiguration() method and return an instance of your Configuration.

Exporting the Configuration for IDEs

When the container is compiled in debug mode, FrameworkBundle exports the configuration tree of every registered bundle into the config/reference.php and config/schema.json files of the application. IDEs and static analyzers use them to autocomplete and validate the application configuration (see Configuring Symfony).

8.2

The config/schema.json file was introduced in Symfony 8.2.

These files are meant for applications only, so your bundle must not rely on their contents.

Both files are built from the configuration tree, so avoid changing the shape of the configuration in beforeNormalization() closures. The exports only include the input type checked by methods like ifString() or ifArray(), while closures added with methods like always() or ifTrue() are ignored. In both cases, the alternative shapes that your closures accept are not described, so tools can't autocomplete or validate those options. Prefer defining explicit nodes so that the accepted structure appears in both files.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version