Create a Symfony2 DiC Extension

Published on

May 11, 2011

how2tips

May 12, 2011 − In this article I will show you how to write an extension of the Symfony2 Dependency Injection Container (his little nickname is DiC). It will allow you to besome SuperMagicMan, that is to say to use $this->get('my.beautiful.service') in your controllers.

What will you learn, and what you won't

In this article I will show you how to write an extension of the Symfony2 Dependency Injection Container (his little nickname is DiC). It will allow you to use $this->get('my.beautiful.service') in your controllers.

I will not teach you why you should use DI and how to use it in details. There are some good docs for it on Fabien's blog and in the official Symfony2 documentation.

What is it for ?

You've got some awesome functionality in your Bundle (and that's pretty cool) and you want to access those functionalities with the Dependency Injection Container of Symfony2. Let's say you've created a powerful String toolbox that is able to convert a string to lowercase or uppercase.

Here could be this class:

<?php
//src/Knp/TestBundle/Toolbox/StringToolbox.php

namespace KnpTestBundleToolbox;

class StringToolbox
{
    public function toUpper($aString)
    {
        return strtoupper($aString);
    }

    public function toLower($aString)
    {
        return strtolower($aString);
    }
}

What? It's not useful or awesome? It has nothing to do in an instance of any PHP class? You're certainly right. But, hey, get a life, it's just an example to write a DI extension.

Creation of the extension file

First we should take some time to create required directories and files. Create the directory DependencyInjection under your Bundle directory. Then create BundleNameExtension.php under this new directory. You're ready to go.

In my case I will create a KnpTestExtension.php under src/Knp/TestBundle/DependencyInjection with the following code inside:

<?php

namespace KnpTestBundleDependencyInjection;

use SymfonyComponentDependencyInjectionContainerBuilder;
use SymfonyComponentDependencyInjectionLoaderXmlFileLoader;
use SymfonyComponentHttpKernelDependencyInjectionExtension;
use SymfonyComponentConfigFileLocator;

class KnpTestExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');
    }

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

This file is quite simple, we just define a new extension named KnpTestExtension and we override 2 methods of the base Extension class. The first method load is used to load a XML file with services definitions inside (it could be another format if you want like YAML, but best practices say that services should be defined in XML files). The second method getAlias is used to define an alias for your extension. It will be used in the configuration file when referring to this extension (it's not mandatory but personnally, I prefer to do it).

Definition of the services

As you should already have noticed in the load method, we will need a services.xml under the Resources/config directory of our bundle. So let's create it.

<?xml version="1.0" ?>

<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">

    <services>

    </services>
</container>

Ok, it's quite empty now (that's where you see the powerfulness of XML verbosity ;-) ). We will try to fill it, and to understand what we will put inside.

Let's define our beautiful StringToolbox class as a service

<?xml version="1.0" ?>

<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="toolbox.string.class">KnpTestBundleToolboxStringToolbox</parameter>
    </parameters>

    <services>
        <service id="toolbox.string" class="%toolbox.string.class%"></service>
    </services>

</container>

And ... that's all folks! You should now be able to access your class inside a controller for example with this code:

<?php

public function testAction($name)
{
    $toolbox = $this->get('toolbox.string');

    return array('name' => $toolbox->toUpper($name));
}

Is'nt it beautiful? You can notice that I used a parameter to define the name of the StringToolbox class. Once again it's not mandatory, but for resusability purpose it's better to do it.

Configuring services

Let's say that you added a more awesome method to your StringToolbox class called transform. This method should be able to transform a string (to lowercase or to uppercase) based on a parameter passed as an argument to the constructor. Here is the new definition of our class:

<?php
//src/Knp/TestBundle/Toolbox/StringToolbox.php

namespace KnpTestBundleToolbox;

class StringToolbox
{
    private $transformation;

    public function __construct($transformation = 'upper')
    {
        $this->transformation = $transformation;
    }

    public function toUpper($aString)
    {
        return strtoupper($aString);
    }

    public function toLower($aString)
    {
        return strtolower($aString);
    }

    public function transform($aString)
    {
        if($this->transformation == 'upper')
        {

            return $this->toUpper($aString);
        }
        else if($this->transformation == 'lower')
        {

            return $this->toLower($aString);
        }
        else
        {

            return $aString;
        }

    }
}

We should tell our services.xml that our class takes an argument in the constructor now. Let's do it.

<?xml version="1.0" ?>

<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="toolbox.string.class">KnpTestBundleToolboxStringToolbox</parameter>
        <parameter key="toolbox.string.transform">upper</parameter>
    </parameters>

    <services>
        <service id="toolbox.string" class="%toolbox.string.class%">
            <argument>%toolbox.string.transform%</argument>
        </service>
    </services>

</container>

I've just added a new parameter called toolbox.string.transform with a default value of 'upper'. It will be passed as the first argument to the constructor method of our class. Now what we want to do is to be able to confirure our transformation through app/config/config.yml in this way:

knp_test:
    transform: lower

We will need a new modification in the KnpTestExtension.php file. Here it is:

<?php

//...

public function load(array $configs, ContainerBuilder $container)
{
    $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
    $loader->load('services.xml');

    //Merge configs
    $config = array();
    foreach($configs as $c) {
        $config = array_merge($config, $c);
    }

    if (isset($config['transform'])) {
        $container->setParameter('toolbox.string.transform', $config['transform']);
    }
}

//..

Notice the new 'merge config' code first. It's just a convenience method to get rid of a level of array imbrication. Because by default, each index of the array represents a loaded config file (0 is often app/config/config.yml). Merging the arrays will allow you to override some configuration values in your different config files (prod, dev, ...).

Then we check if the transform value is set and we use it to override our default DiC parameter. We're done. Our class is configured according to the value of transform inside app/config/config.yml. Notice that you could use another service as an argument of the StringToolbox class. It's explain in the Official documentation if you need it.

You're now able to use it in your controllers:

<?php

public function testAction($name)
{
    $toolbox = $this->get('toolbox.string');

    return array('name' => $toolbox->transform($name));
}

and to configure the type of transformation (upper or lower) inside your app/config/config.yml file.

Written by

KNP Labs
KNP Labs

Comments