How to make your Services use Tags

Several of Symfony2’s core services depend on tags to recognize which services should be loaded, notified of events, or handled in some other special way. For example, Twig uses the tag twig.extension to load extra extensions.

But you can also use tags in your own bundles. For example in case your service handles a collection of some kind, or implements a “chain”, in which several alternative strategies are tried until one of them is successful. In this article I will use the example of a “transport chain”, which is a collection of classes implementing \Swift_Transport. Using the chain, the Swift mailer may try several ways of transport, until one succeeds. This post focuses mainly on the dependency injection part of the story.

To begin with, define the TransportChain class:

namespace Acme\MailerBundle;

class TransportChain
{
    private $transports;

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

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

Then, define the chain as a service:

  • YAML
    # src/Acme/MailerBundle/Resources/config/services.yml
    parameters:
        acme_mailer.transport_chain.class: Acme\MailerBundle\TransportChain
    
    services:
        acme_mailer.transport_chain:
            class: %acme_mailer.transport_chain.class%
    
  • XML
    <!-- src/Acme/MailerBundle/Resources/config/services.xml -->
    
    <parameters>
        <parameter key="acme_mailer.transport_chain.class">Acme\MailerBundle\TransportChain</parameter>
    </parameters>
    
    <services>
        <service id="acme_mailer.transport_chain" class="%acme_mailer.transport_chain.class%" />
    </services>
    
  • PHP
    // src/Acme/MailerBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setParameter('acme_mailer.transport_chain.class', 'Acme\MailerBundle\TransportChain');
    
    $container->setDefinition('acme_mailer.transport_chain', new Definition('%acme_mailer.transport_chain.class%'));
    

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 the addTransport() method. As an example we add the following transports as services:

  • YAML
    # src/Acme/MailerBundle/Resources/config/services.yml
    services:
        acme_mailer.transport.smtp:
            class: \Swift_SmtpTransport
            arguments:
                - %mailer_host%
            tags:
                -  { name: acme_mailer.transport }
        acme_mailer.transport.sendmail:
            class: \Swift_SendmailTransport
            tags:
                -  { name: acme_mailer.transport }
    
  • XML
    <!-- src/Acme/MailerBundle/Resources/config/services.xml -->
    <service id="acme_mailer.transport.smtp" class="\Swift_SmtpTransport">
        <argument>%mailer_host%</argument>
        <tag name="acme_mailer.transport" />
    </service>
    
    <service id="acme_mailer.transport.sendmail" class="\Swift_SendmailTransport">
        <tag name="acme_mailer.transport" />
    </service>
    
  • PHP
    // src/Acme/MailerBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $definitionSmtp = new Definition('\Swift_SmtpTransport', array('%mailer_host%'));
    $definitionSmtp->addTag('acme_mailer.transport');
    $container->setDefinition('acme_mailer.transport.smtp', $definitionSmtp);
    
    $definitionSendmail = new Definition('\Swift_SendmailTransport');
    $definitionSendmail->addTag('acme_mailer.transport');
    $container->setDefinition('acme_mailer.transport.sendmail', $definitionSendmail);
    

Notice the tags named “acme_mailer.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 build() method to the AcmeMailerBundle class:

namespace Acme\MailerBundle;

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

use Acme\MailerBundle\DependencyInjection\Compiler\TransportCompilerPass;

class AcmeMailerBundle 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 acme_mailer.transport will be added to the TransportChain class by calling the addTransport() method. The TransportCompilerPass should look like this:

namespace Acme\MailerBundle\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('acme_mailer.transport_chain')) {
            return;
        }

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

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

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

Note

By convention, tag names consist of the name of the bundle (lowercase, underscores as separators), followed by a dot, and finally the “real” name, so the tag “transport” in the AcmeMailerBundle should be: acme_mailer.transport.

The Compiled Service Definition

Adding the compiler pass will result in the automatic generation of the following lines of code in the compiled service container. In case you are working in the “dev” environment, open the file /cache/dev/appDevDebugProjectContainer.php and look for the method getTransportChainService(). It should look like this:

protected function getAcmeMailer_TransportChainService()
{
    $this->services['acme_mailer.transport_chain'] = $instance = new \Acme\MailerBundle\TransportChain();

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

    return $instance;
}