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;
}
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.Thanks! There is already a good article about this in the Dependency Injection Component documentation.
Very good! Thanks for reporting it!
This should definitely be a cookbook article! Thanks!
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.
"spent lots of time to search, findally i get it, useful finfo me for me!"