Skip to content

Commit b183b93

Browse files
matthiasnobackweaverryan
authored andcommitted
Concept version of cookbook article about service tags
1 parent fd0e0f7 commit b183b93

File tree

3 files changed

+190
-0
lines changed

3 files changed

+190
-0
lines changed

cookbook/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Cookbook
3737
service_container/factories
3838
service_container/parentservices
3939
service_container/scopes
40+
service_container/tags
4041
configuration/pdo_session_storage
4142

4243
bundles/best_practices

cookbook/map.rst.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
* :doc:`/cookbook/service_container/factories`
4747
* :doc:`/cookbook/service_container/parentservices`
4848
* :doc:`/cookbook/service_container/scopes`
49+
* :doc:`/cookbook/service_container/tags`
4950
* :doc:`/cookbook/configuration/pdo_session_storage`
5051

5152
* **Bundles**

cookbook/service_container/tags.rst

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
How to Make Your Services Use Tags
2+
==================================
3+
4+
Several of Symfony2's core services depend on tags to recognize which services
5+
should be loaded, notified of events, etc. For example, Twig uses the tag
6+
``twig.extension`` to load extra extensions.
7+
8+
But you can also use tags in your own bundles. For example in case your service
9+
handles a collection of some kind, or implements a "chain", in which several alternative
10+
strategies are tried until one of them is successful. In this article I will use the example
11+
of a "transport chain", which is a collection of classes implementing ``\Swift_Transport``.
12+
Using the chain, the Swift mailer may try several ways of transport, until one succeeds.
13+
This post focuses mainly on the dependency injection part of the story.
14+
15+
To begin with, define the ``TransportChain`` class.
16+
17+
namespace Acme\TransportBundle;
18+
19+
class TransportChain
20+
{
21+
private $transports;
22+
23+
public function __construct()
24+
{
25+
$this->transports = array();
26+
}
27+
28+
public function addTransport(\Swift_Transport $transport)
29+
{
30+
$this->transports[] = $transport;
31+
}
32+
}
33+
34+
Then, define the chain as a service:
35+
36+
.. configuration-block::
37+
38+
.. code-block:: yaml
39+
40+
# src/Acme/TransportBundle/Resources/config/services.yml
41+
parameters:
42+
transport_chain.class: Acme\TransportBundle\TransportChain
43+
44+
services:
45+
transport_chain:
46+
class: %transport_chain.class%
47+
48+
.. code-block:: xml
49+
50+
<!-- src/Acme/TransportBundle/Resources/config/services.xml -->
51+
52+
<parameters>
53+
<parameter key="transport_chain.class">Acme\TransportBundle\TransportChain</parameter>
54+
</parameters>
55+
56+
<services>
57+
<service id="transport_chain" class="%transport_chain.class%" />
58+
</services>
59+
60+
.. code-block:: php
61+
62+
// src/Acme/TransportBundle/Resources/config/services.php
63+
use Symfony\Component\DependencyInjection\Definition;
64+
65+
$container->setParameter('transport_chain.class', 'Acme\TransportBundle\TransportChain');
66+
67+
$container->setDefinition('transport_chain', new Definition('%transport_chain.class'));
68+
69+
Define Services with a Custom Tag
70+
---------------------------------
71+
72+
Now we want several of the ``\Swift_Transport`` classes to be instantiated and added to the chain
73+
automatically using the ``addTransport()`` method. As an example we add the following transports
74+
as services:
75+
76+
.. configuration-block::
77+
78+
.. code-block:: yaml
79+
80+
services:
81+
transport.smtp:
82+
class: \Swift_SmtpTransport
83+
arguments:
84+
- %mailer_host%
85+
tags:
86+
- { name: mailer.transport }
87+
transport.sendmail:
88+
class: \Swift_SendmailTransport
89+
tags:
90+
- { name: mailer.transport }
91+
92+
.. code-block:: xml
93+
94+
<service id="transport.smtp" class="\Swift_SmtpTransport">
95+
<argument>%mailer_host%</argument>
96+
<tag name="mailer.transport" />
97+
</service>
98+
99+
<service id="transport.sendmail" class="\Swift_SendmailTransport">
100+
<tag name="mailer.transport" />
101+
</service>
102+
103+
.. code-block:: php
104+
105+
// src/Acme/TransportBundle/Resources/config/services.php
106+
use Symfony\Component\DependencyInjection\Definition;
107+
108+
$definitionSmtp = new Definition('\Swift_SmtpTransport', array('%mailer_host%'));
109+
$definitionSmtp->addTag('mailer.transport');
110+
$container->setDefinition('transport.smtp', $definitionSmtp);
111+
112+
$definitionSendmail = new Definition('\Swift_SendmailTransport');
113+
$definitionSendmail->addTag('mailer.transport');
114+
$container->setDefinition('transport.sendmail', $definitionSendmail);
115+
116+
Notice the tags named "mailer.transport". We want the bundle to recognize these transports
117+
and add them to the chain all by itself. In order to achieve this, we need to
118+
add a ``build()`` method to the ``AcmeTransportBundle`` class:
119+
120+
namespace Acme\TransportBundle;
121+
122+
use Symfony\Component\HttpKernel\Bundle\Bundle;
123+
use Symfony\Component\DependencyInjection\ContainerBuilder;
124+
125+
use Acme\TransportBundle\DependencyInjection\Compiler\TransportCompilerPass;
126+
127+
class AcmeTransportBundle extends Bundle
128+
{
129+
public function build(ContainerBuilder $container)
130+
{
131+
parent::build($container);
132+
133+
$container->addCompilerPass(new TransportCompilerPass());
134+
}
135+
}
136+
137+
Create a ``CompilerPass``
138+
-------------------------
139+
140+
You will have spotted a reference to the not yet existing ``TransportCompilerPass`` class.
141+
This class will make sure that all services with a tag "mailer.transport" will be added to
142+
the ``TransportChain`` class by calling the ``addTransport()`` method.
143+
The ``TransportCompilerPass`` should look like this:
144+
145+
namespace Acme\TransportBundle\DependencyInjection\Compiler;
146+
147+
use Symfony\Component\DependencyInjection\ContainerBuilder;
148+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
149+
use Symfony\Component\DependencyInjection\Reference;
150+
151+
class TransportCompilerPass implements CompilerPassInterface
152+
{
153+
public function process(ContainerBuilder $container)
154+
{
155+
if (false === $container->hasDefinition('transport_chain')) {
156+
return;
157+
}
158+
159+
$definition = $container->getDefinition('transport_chain');
160+
161+
foreach ($container->findTaggedServiceIds('mailer.transport') as $id => $attributes) {
162+
$definition->addMethodCall('addTransport', array(new Reference($id)));
163+
}
164+
}
165+
}
166+
167+
The ``process()`` method checks for the existence of the ``transport_chain`` service,
168+
then looks for all services tagged "mailer.transport". It adds to the definition of the
169+
``transport_chain`` service a call to ``addTransport()`` for each "mailer.transport" service
170+
it has found. The first argument of each of these calls will be the mailer transport service itself.
171+
172+
The Compiled Service Definition
173+
-------------------------------
174+
175+
Adding the compiler pass will result in the automatic generation of the following lines of code
176+
in the compiled service container. In case you are working in the "dev" environment, open the file
177+
``/cache/dev/appDevDebugProjectContainer.php`` and look for the method ``getTransportChainService()``.
178+
It should look like this:
179+
180+
protected function getTransportChainService()
181+
{
182+
$this->services['transport_chain'] = $instance = new \Acme\TransportBundle\TransportChain();
183+
184+
$instance->addTransport($this->get('transport.smtp'));
185+
$instance->addTransport($this->get('transport.sendmail'));
186+
187+
return $instance;
188+
}

0 commit comments

Comments
 (0)