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="https://symfony.com/schema/dic/services"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://symfony.com/schema/dic/services https://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
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Luciano Mammino

Great article, really helped me a lot. I do agree with @Bart van den Burg: you should definitely write a cookbook article.
It would also be good to mention the role of the $attributes array and how you can use tags to pass additional informations.

Matthias Noback

Thanks! There is already a good article about this in the Dependency Injection Component documentation.

Luciano Mammino

Very good! Thanks for reporting it!

Bart van den Burg

This should definitely be a cookbook article! Thanks!

Alexander Trauzzi (@Omega_)

Great post! I recently needed to use tagging as a way to allow bundles to inject their administration interfaces into my administration panel.

I ended up doing very similar to you, however I feel like Symfony2's API might be lacking when it comes to tags:
https://github.com/symfony/...

A way to get all service instances of a specific tag as an array would certainly not be unreasonable to expect from such a concept.

Alma

"spent lots of time to search, findally i get it, useful finfo me for me!"