Symfony2: Application configuration for teams

Posted on by Matthias Noback

There's a much more detailed chapter about this subject in my book A Year With Symfony.

A Symfony2 application provides developers with several ways to manipulate its behavior. First of all, it is possible to define different environments for different scenarios:

  • A "prod" environment to be used when the web application is on the live server

  • A "dev" environment used while developing the application. Generated parts of the application are regenerated when one of the files the generation was based on has changed

  • A "test" environment used when running functional unit tests/li>

The environment is first defined when the Kernel is created:

// in web/app_dev.php
$kernel = new AppKernel('dev', true);

Based on the environment, only one of the config files in app/config/ is loaded:

// in app/AppKernel.php
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');

From the environment-specific config file, other files are imported:

# in app/config_dev.yml
imports:
    - { resource: config.yml }

# in app/config.yml
imports:
    - { resource: parameters.yml }

Values in app/config/parameters.yml end up as parameters in the service container:

# in app/config/parameters.yml
parameters:
    administrator_email_address: matthiasnoback@gmail.com
// in a standard controller
$this->container->getParameter('%administrator_email_address%');

Parameters in a team

The first problem arises when not every member of your team uses the same database credentials. The solution is quite simple: make sure parameters.yml will be ignored by your version control system (e.g. Git):

# in .gitignore
app/config/parameters.yml

Then, make sure you have a app/config/parameters.yml.dist file. This file should...

  • contain all the required parameters

  • provide example values for each parameter

  • explain what each parameter is used for

For example:

# in app/config/parameters.yml.dist
parameters:
    # filled in contact forms will be sent to this address
    administrator_email_address: username@domain.com

    # go to https://getsecrethere.com/ to retrieve a personalized secret
    api_secret: XXX

It requires some discipline of all the team members to keep this file up-to-date: when you dream up a new parameter, add it not only to your own parameters.yml but also to the dist file. Also, don't forget to remove parameters that are no longer used.

Christophe Coevoet has made a nice tool called ParameterHandler which asks you to provide values for your parameters.yml when they are defined in parameters.yml.dist but missing in your personal parameters.yml file.

More advanced configuration

Since parameters are a nice and familiar way to one-dimensionally define variables for your project, the file might end up containing way too many parameters, which may or may not be grouped semantically:

# in app/config/parameters.yml
parameters:
    api_secret: XXX
    api_client_id: YYY
    api_url: ZZZ

Then in your code you may be tempted to inject the entire service container into some service and do a quick getParameter():

$apiSecret = $this->container->getParameter('api_secret');
$apiClientId = $this->container->getParameter('api_client_id');
$apiUrl = $this->container->getParameter('api_url');

First of all, it is very likely that your API client class would be able to accept these values as service arguments:

# in YourBundle/Resources/config/services.yml
services:
    api_client:
        class: YourBundle\ApiClient
        arguments: [%api_url%, %api_client_id%, %api_secret%]

Second, it would be a good idea to semantically define a configuration for your bundle. Start by creating a Configuration class.

// in YourBundle/DependencyInjection/Configuration.php

namespace YourBundle\DependencyInjection;

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

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

        $rootNode = $treeBuilder->root('your_bundle');
        $rootNode
            ->children()
                ->arrayNode('api')
                    ->children()
                        ->scalarNode('url')->end()
                        ->scalarNode('client_id')->end()
                        ->scalarNode('client_secret')->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }
}

Then you should process the configuration from within your bundle's extension:

// in YourBundle/DependencyInjection/YourBundleExtension.php
namespace YourBundle\DependencyInjection;

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

class YourBundleExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        // ...

        $processedConfig = $this->processConfiguration(new Configuration(), $configs);
    }
}

Now when you add this to app/config.yml:

your_bundle:
    api:
        url: https://apiurl.com
        client_id: 15636
        client_secret: ud732b3mfd

The above-mentioned $processedConfig will be an array containing the given values under the key "api". After that, you can choose which of these config values to set on the container:

$container->setParameter('your_bundle.api.client_secret', $processedConfig['api']['client_secret']);

But you may also dynamically use $processedConfig to configure your own services:

$container->getDefinition('your_bundle.api_client')->addMethodCall('setClientId', array($processedConfig['api']['client_id']));

Semantic configuration for teams

Of course, team members might have to use their own API credentials, or use a local URL instead of the real URL. You don't want them to temporarily modify the config file (since changes might accidentally be committed to the repository). There are many strategies to prevent hard-coding this information in config files. First of all, you could use parameters from parameters.yml inside your configuration files:

# in app/config/config_dev.yml
your_bundle:
    api:
        url: https://apiurl.com
        client_id: %api_client_id%
        client_secret: %api_client_secret%

Another option would be to leverage the "imports" key in the config file:

# in app/config/config_dev.yml
imports:
    - { resource: local.yml }

You could then override certain configuration values defined in other config files:

# in app/config/local.yml
your_bundle:
    url: http://localhost

The "local" flavor of your application should be excluded from version control, e.g.

# in .gitignore
app/config/local.yml

You may also provide a local.yml.dist with some (commented out) suggestions for modifying your application's behavior.

Finally, make sure that when the Kernel loads the configuration, it will not fail when the local.yml file does not exist. You can accomplish this by using the "ignore_errors" key:

# in app/config/config_dev.yml
imports:
    - { resource: local.yml, ignore_errors: true }

This way, it is not necessary for a live application to have a local.yml file.

What to put where?

Whenever you find yourself using a single value or a set of values which change the behavior of your application, make it a configuration value. As we have seen, there are many ways to propagate this choice to your team members:

  • Use a parameter for it and either retrieve it directly from the container, or preferably inject it into your services

  • Put it in your bundle's configuration, and potentially let it be overridden by personal config files or by parameters

But where to put which values? Well...

When a config value varies per instance of the application (i.e. is probably different for each computer that will run it), it should definitely be in app/config/parameters.yml. When a group of config values are related to each other, they should be defined (including rules for validation) in a bundle configuration file. When you have several flavors of your application installed on different servers, or developers need to have a way to locally override configuration (for their convenience) it would be a good idea to put the settings that vary between these servers in a separate file, which is imported by the main config file.

You will find more information about hierarchical, well-defined and validated configuration values in the documentation of the Config Component and this cookbook article.

PHP Symfony2 configuration