サービスにタグを使用する方法

Symfony2 のコアサービスには、どのサービスが、ロードされるべきか、イベントに通知されるべきか、何か特別な方法で処理すべきか、といったことを決めるためのタグに依存しているものがあります。例えば、 Twig は twig.extension というタグを使用し、追加のエクステンションをロードしています。

これらのタグは、あなたの開発するバンドルにも使用することができます。例えば、あなたのサービスが何かのコレクションを処理したり、”チェーン” を実装して、成功するまで代替戦略を試みたりするときなどです。この記事では、 \Swift_Transport を実装するクラスのコレクションである “transport chain” の例を使用します。このチェーンを使用して、成功するまで Swiftmailer がいくつかのトランスポートの使用を試みます。この内容は、主に DI に関して着目しています。

まず始めに、 TransportChain クラスを定義します

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
    // 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() メソッドを使用し自動的にチェーンへ追加していきます。例として、次のトランスポートをサービスとして追加します。

  • 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
    // 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” として名付けられたタグを通知します。バンドルにこれらのトランスポートを認識させ、自身でこれらのトランスポートをチェーンに追加させるようにしましょう。そのために、 AcmeMailerBundle クラスに build() メソッドを追加してください。

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 クラスへのリファレンスに気づくでしょう。このクラスは、 addTransport() メソッドを呼び acme_mailer.transport でタグ付けされた全てのサービスを TransportChain クラスに追加させます。 TransportCompilerPass は次のようになります。

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” サービスを acme_mailer.transport_chain の定義に加えます。これらの呼び出しの最初の引数は、メーラートランスポートサービスそのものになります。

Note

慣習として、タグ名は、バンドルの名前(小文字で区切りはアンダースコア)にドットが次に来て、 “実際の” 名前をつなげます。つまり、 AcmeMailerBundle の “transport” タグであれば acme_mailer.transport になります。

コンパイルされたサービスの定義

コンパイルを通すと、コンパイルされたサービスコンテナに次の行を自動的に生成することになります。 “dev” 環境で動かしているのであれば、 /cache/dev/appDevDebugProjectContainer.php を開き、 getTransportChainService() メソッドを調べてみてください。次のようになっているはずです。

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;
}