Symfony2 service container: how to make your service use tags
Matthias Noback
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;
}