Symfony2 service container: how to make your service use tags

Posted on by Matthias Noback

After writing this article I've modified it to become part of the official Symfony documentation as a Cookbook article. Afterwards it has been moved to the Dependency Injection Component documentation.

First of all: dependency injection is GREAT!

Several of Symfony2's core services depend on tags to recognize which user-defined services should be loaded, notified of events, etc. For example, Twig uses the tag twig.extension to load extra extensions. It would also be great to use tags in case your service implements some kind of "chain", in which several alternative strategies are tried until one of them is successful. In this post I use the example of a so-called "TransportChain". This chain consists of a set of classes which implement the Swift_Transport interface. Using the chain, the mailer may try several ways of transport, until one succeeds. This post focuses on the "dependency injection" part of the story. The implementation of the real TransportChain is left to the reader (as an exercise ;)).

To begin with, we define the TransportChain class in /Acme/TransportBundle/TransportChain.php:

namespace Acme\TransportBundle;

class TransportChain
{
    protected $transports;

    public function __construct()
    {
        $this->transports = array();
    }

    public function addTransport(\Swift_Transport  $transport)
    {
        $this->transports[] = $transport;
    }
}

In /Acme/Resources/config/services.xml we define the chain 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="transport_chain.class">Acme\TransportBundle\TransportChain</parameter>
    </parameters>

    <services>
        <service id="transport_chain" />
    </services>
</container>

Define services with a custom tag

Now we want several of the Swift_Transport classes to be instantiated and added to the chain automatically using addTransport(). As an example we add the following transports as services in services.xml:

<services>
    <!-- ... --->

    <service id="transport.smtp">
        <argument>%mailer_host%</argument>
        <tag name="transport" />
    </service>

    <service id="transport.sendmail">
        <tag name="transport" />
    </service>
</services>

Notice the tags "transport". We want the bundle to recognize these transports and add them to the chain all by itself. In order to achieve this, we need to add a method to the AcmeTransportBundle class in /Acme/TransportBundle/AcmTransportBundle.php (don't forget to add the extra "use" statements):

namespace Acme\TransportBundle;

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

use Acme\TransportBundle\DependencyInjection\Compiler\TransportCompilerPass;

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

        $container->addCompilerPass(new TransportCompilerPass());
    }
}

Create a CompilerPass

You will have spotted a reference to the not yet existing TransportCompilerPass class. This class will make sure that all services with a tag "transport" will be added to the TransportChain class by calling the addTransport() method. Create the file /Acme/TransportBundle/DependencyInjection/Compiler/TransportCompilerPass.php containing this code:

namespace Acme\TransportBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class TransportCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (false === $container->hasDefinition('transport_chain')) {
            return;
        }

        $definition = $container->getDefinition('transport_chain');

        foreach ($container->findTaggedServiceIds('transport') as $id => $attributes) {
            $definition->addMethodCall('addTransport', array(new Reference($id)));
        }
    }
}

The process() method checks for the existence of the transport_chain service, then looks for all services tagged "transport". It adds to the definition of the transport_chain service a call to addTransport for every "transport" service it has found. The first argument of each of these calls will be the transport service itself.

The compiled service definition

All this will result in the automatic generation of the following lines of code in the compiled service container (see /cache/dev/appDevDebugProjectContainer.php).

    protected function getTransportChainService()
    {
        $this->services['transport_chain'] = $instance = new \Acme\TransportBundle\TransportChain();

        $instance->addTransport($this->get('transport.smtp'));
        $instance->addTransport($this->get('transport.sendmail'));

        return $instance;
    }
PHP Symfony2 dependency injection service container compiler pass bundle