Определённые сервисы ядра Symfony 2 зависят от тегов, по которым определяют: какие сервисы должны быть загружены, быть оповещены о наступлении события либо применены по другому назначению. Например, Twig использует twig.extension
для загрузки расширений.
Но теги можно использовать и по своему назначению в собственных бандлах. К примеру, ваш сервис работает с какой-либо коллекцией данных или реализовывает “цепочку” действий, которые выполняются последовательно до наступления положительного результата. В этой статье я приведу пример “транспортной цепочки” — коллекция классов реализующих \Swift_Transport
. Используя такую цепочку, Swift mailer (служба отправки электронных писем в Symfony2) использует несколько способов отправки писем, до тех пор пока письмо не будет отправлено. В данной статье уделено основное внимание внедрению зависимостей в проект.
Для начала создадим класс TransportChain
:
<?php
namespace Acme\MailerBundle;
class TransportChain
{
private $transports;
public function __construct()
{
$this->transports = array();
}
public function addTransport(\Swift_Transport $transport)
{
$this->transports[] = $transport;
}
}
Теперь объявим этот класс в качестве сервиса:
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
<?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%'));
Объявление сервиса с пользовательским тегом
Было бы здорово, чтобы несколько классов \Swift_Transport
объявлялись и автоматически добавлялись в цепочку при помощи метода addTransport()
. Как пример добавим следующие методы доставки (smtp, sendmail) в качестве сервисов:
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
<?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);
Обратите внимание на тег acme_mailer.transport
. Мы хотим чтобы бандл автоматически определял эти способы доставки и добавлял их в цепочку. Для того, чтобы этого добиться, надо добавить метод build()
в класс AcmeMailerBundle:
<?php
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());
}
}
Создаем класс CompilerPass
Вы наверняка заметили упоминание несуществующего пока класса TransportCompilerPass. Он отвечает за загрузку всех сервисов с тегом acme_mailer.transport
в класс TransportChain через метод addTransport()
. Вот и сам класс TransportCompilerPass:
<?php
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)));
}
}
}
Метод process()
сначала проверяет наличие сервиса acme_mailer.transport_chain
, а затем находит все службы с тегом acme_mailer.transport
. Так же он выполняет метод addTransport()
для каждой найденной службы с тегом acme_mailer.transport
. В качестве первого параметра передаётся сама служба доставки.
По соглашению, имена тегов должны начинаться с названия бандла (нижний регистр, подчеркивание в качестве разделителя), затем должна стоять точка, а в конце — “настоящее” название. Так, тег “transport” в бандле AcmeMailerBundle должен выглядеть следующим образом:
acme_mailer.transport
Объявление скомпилированного сервиса
После пропуска через компилятор, в окончательной версии оболочки сервиса, появятся следующие строки. Если вы используете среду dev, то загляните в файл /cache/dev/appDevDebugProjectContainer.php
и найдите там метод getTransportChainService()
. Если вы всё сделали правильно, то он должен выглядеть следующим образом:
<?php
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;
}