After a few weeks, we continue with the Symfony2 components series. This time we are going to be talking about the Config component, which helps you to load and validate configuration values, regardless of their source.

The Nazi’s Enigma Machine had 158,962,555,217,826,360,000 different configurations to cypher messages

Let’s imagine we want to create a blog generator system, which will take a few configuration parameters such as title, description, number of posts shown in the main page, social media icons, and whether the blog will have a RSS channel or not. With that in mind, we could create a YAML configuration file like this:

blog:
    title: My blog
    description: This is just a test blog
    rss: true
    posts_main_page: 2
    social:
        twitter:
            url: http://twitter.com/raulfraile
            icon: twitter.png
        sensiolabs_connect:
            url: https://connect.sensiolabs.com/profile/raulfraile
            icon: sensiolabs_connect.png

Now, we would need to parse the file, check that the required fields are present, set a default value for optional fields and perform by-field validations, such as the rss field must be a boolean or posts_main_page an integer between 1 and 10. In addition, we need to do it every time the script runs, unless we use any sort of caching system. It gets even more trickier if we want to support additional formats like INI, XML or JSON.

To avoid all this we use the config component, so you don’t have to code all of this yourself. It is simple, well tested, and flexible enough to cover the needs of any project.

Architecture

The component is divided in two main parts:

  • Definition of the configuration values hierarchy

    The component permits to define the format of the configuration resource, which basically can be anything, from a simple INI file to a something more esoteric like protocol-specific configuration messages. Using the TreeBuilder builder we can define the types of the values, make them required/optional and set a default value.
  • Locate, load and process configuration resources

    Once the configuration resource has been defined, must be located, loaded and processed to check that the values comply with the defined format. At the end, the component will give us a plain array with the validated values or an exception if there is an error.

Example

Let’s go back to our example. We wanted to create a highly configurable blog generator system, so it’s time to define the configuration values. For that, we use a TreeBuilder instance, which provides an easy DSL for that.

<?php

namespace RaulFraile\Config;

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('blog');

        $rootNode
            ->children()
                ->scalarNode('title')
                    ->isRequired()
                ->end()
                ->scalarNode('description')
                    ->defaultValue('')
                ->end()
                ->booleanNode('rss')
                    ->defaultValue(false)
                ->end()
                ->integerNode('posts_main_page')
                    ->min(1)
                    ->max(10)
                    ->defaultValue(5)
                ->end()
                ->arrayNode('social')
                    ->prototype('array')
                        ->children()
                            ->scalarNode('url')->end()
                            ->scalarNode('icon')->end()
                        ->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }
}

Whoa! Don’t worry too much if it is the first time you see this kind of code in PHP, DSLs always look a bit weird in PHP. What we are doing here is to define a root node, called “blog”, and the format of all the configuration values – nodes -. We expect a scalar value for the “title” and is required, “description” is optional and will be empty if not present, “rss” must be a boolean – true/false – and will be false by default while “posts_main_page” must be an integer between 1 and 10, with 5 by default.

Ok, we have the definition, let’s load and process it. As the configuration resource can be anything, we need to define how to convert that resource into a plain array, that will be validated and processed later using our definition. There must be one class for each format, so if we allow YAML and XML files, we would need to create two classes. In our example we’ll use only YAML for simplicity:

<?php

namespace RaulFraile\Config;

use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Yaml\Yaml;

class YamlConfigLoader extends FileLoader
{
    public function load($resource, $type = null)
    {
        $configValues = Yaml::parse($resource);

        return $configValues;
    }

    public function supports($resource, $type = null)
    {
        return is_string($resource) && 'yml' === pathinfo(
            $resource,
            PATHINFO_EXTENSION
        );
    }
}

As you see, is very simple. The supports method is used by the LoaderResolver to know wether the resource can be load with that specific loader. The load method just converts the YAML file into an array using the Yaml component.

Finally, we make use of the definition and the loader to process the configuration values:

<?php

use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Definition\Processor;
use RaulFraile\Config\YamlConfigLoader;
use RaulFraile\Config\Configuration;

include_once __DIR__. '/vendor/autoload.php';

// directories of config files
$directories = array(__DIR__.'/config');
$locator = new FileLocator($directories);

// convert the config file into an array
$loader = new YamlConfigLoader($locator);
$configValues = $loader->load($locator->locate('config.yml'));

// process the array using the defined configuration
$processor = new Processor();
$configuration = new Configuration();
try {
    $processedConfiguration = $processor->processConfiguration(
        $configuration,
        $configValues
    );

    // configuration validated
    var_dump($processedConfiguration);
} catch (Exception $e) {
    // validation error
    echo $e->getMessage() . PHP_EOL;
}

Let’s see what this code is doing. First, we define an array of directories where the configuration files could be, and using the FileLocator object look for the config.yml file in those folders. Then, we create the YamlConfigLoader object, which returns an array with the configuration values. Eventually, the array with the raw values is processed using our definition.

The output of this script shows the processed array:

array(5) {
  'title' =>
  string(7) "My blog"
  'description' =>
  string(24) "This is just a test blog"
  'rss' =>
  bool(true)
  'posts_main_page' =>
  int(2)
  'social' =>
  array(2) {
    'twitter' =>
    array(2) {
      'url' =>
      string(29) "http://twitter.com/raulfraile"
      'icon' =>
      string(11) "twitter.png"
    }
    'sensiolabs_connect' =>
    array(2) {
      'url' =>
      string(49) "https://connect.sensiolabs.com/profile/raulfraile"
      'icon' =>
      string(22) "sensiolabs_connect.png"
    }
  }
}

If we change the config.yml and remove the “rss” and “posts_main_page” fields, we will get the default values:

array(5) {
  ...
  'rss' =>
  bool(false)
  'posts_main_page' =>
  int(5)

Caching

If the configuration is complex, having to parse and process the configuration resources in every request can be a time-consuming task. The config component makes caching extremely easy by looking at the “last modified” timestamp of the resources.

To enable caching in our example we only need a few more lines:

<?php

use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Definition\Processor;
use RaulFraile\Config\YamlConfigLoader;
use RaulFraile\Config\Configuration;

include_once __DIR__. '/vendor/autoload.php';

$cachePath = __DIR__.'/cache/config.php';
$configFile = 'config.yml';

// the second argument is to enable/disable debugging
$cache = new ConfigCache($cachePath, true);

if (!$cache->isFresh()) {
    // directories of config files
    $directories = array(__DIR__.'/config');
    $locator = new FileLocator($directories);

    // convert the config file into an array
    $loader = new YamlConfigLoader($locator);
    $configFilePath = $locator->locate($configFile);
    $configValues = $loader->load($configFilePath);
    $resource = new FileResource($configFilePath);

    // process the array using the defined configuration
    $processor = new Processor();
    $configuration = new Configuration();
    try {
        $processedConfiguration = $processor->processConfiguration(
            $configuration,
            $configValues
        );

        // serialize the config array and save it
        $cache->write(serialize($processedConfiguration), array($resource));
    } catch (Exception $e) {
        // validation error
        echo $e->getMessage() . PHP_EOL;
    }

}

The instance of the ConfigCache class checks if the cache file exists and then compares the last modification timestamp to detect if the cache is fresh or not. Notice that when we write the cache file, we are saving the list of resources too to be able to compare them in next requests.

Multiple loaders

Allowing different formats is as easy as adding a new loader that supports the new format. In our example, if we wanted to support XML configuration files, we would add the loader and use the resolver:

<?php

namespace RaulFraile\Config;

use Symfony\Component\Config\Loader\FileLoader;

class XmlConfigLoader extends FileLoader
{
    public function load($resource, $type = null)
    {
        // parse xml

        return $configValues;
    }

    public function supports($resource, $type = null)
    {
        return is_string($resource) && 'xml' === pathinfo(
            $resource,
            PATHINFO_EXTENSION
        );
    }
}
$loaderResolver = new LoaderResolver(array(
    new YamlConfigLoader($locator),
    new XmlConfigLoader($locator)
));
$delegatingLoader = new DelegatingLoader($loaderResolver);
$configValues = $delegatingLoader->load($locator->locate('config.xml'));

Generate reference configuration

The component provides a couple of dumpers to generate reference configuration to add to your documentation.

<?php
...
use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper;

$dumper = new YamlReferenceDumper();
echo $dumper->dump($configuration);

Will print out:

blog:
    title:                ~ # Required
    description:          ''
    rss:                  false
    posts_main_page:      5
    social:
        url:                  ~
        icon:                 ~

Advantages

You may be thinking that this is too much work and you would do the same with a couple of functions. Well, this is usually the “cost” of having a good OOP design. On the other hand, using this component gives you lots of advantages:

  • The component is well tested (~80% of code coverage) and actively maintained.
  • Allowing new formats is really easy. Just add a new loader that transforms the resource into a plain array. The same definition will be used for any format. In general, to extend any part of the component – node types/validators, loaders, dumpers… – just have to implement the required interface.
  • Caching works out of the box, with different settings for dev and prod environments.
  • Built-in validators.
  • Generates reference configuration.

More info

Photo: Elliot Brown