Работаем с тегами в Symfony 2

Определённые сервисы ядра 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;
}