Symfony Advent Calendar 2018 13日目の記事です。

同種のクラス群(主に同じinterfaceを実装したクラス群)を大量に使う場合、DIどうしてますか?

# config/services.yaml
services:
  app_sample.foo:
    class: App\Sample\SampleFoo

  app_sample.bar:
    class: App\Sample\SampleBar

  # ...

  App\SampleResolver:
    class: App\SampleResolver
    calls:
      - ["addSample", ["@app_sample.foo"]]
      - ["addSample", ["@app_sample.bar"]]
      - ["addSample", ["@app_sample.baz"]]
      - ["addSample", ["@app_sample.foo2"]]
      - ["addSample", ["@app_sample.foo3"]]
      # ...
      - ["addSample", ["@app_sample.foo100"]]

1個や2個ならまだいいですが、10個を超えるとさすがにサービス定義がめんどくさく&可読性が低くなりますよね。

SymfonyのDependencyInjectionのタグとは

Symfonyを使って開発していると tags: [{ name: console.command }] とか tags: [{name: form.type }] とか tags: [{ name: kernel.event_subscriber }] とか使ったことがありますよね? あれです。
SymfonyのDependencyInjectionにおける組み込みタグは、 特定の種類のサービスにつけておくと自動で必要なサービスに注入してくれる 働きをしています。

タグ、自作できるんです

便利なタグですが、実は自分で作れます!
使いどころとしては、同種のサービス群を大量に他のサービスに注入したいときに役立ちます。

たとえば、先程の可読性が低くなってきたservices.yamlをこのように書き換えることができます。

# config/services.yaml
services:
  app_sample.foo:
    class: App\Sample\SampleFoo
    tags:
      - "myapp.sample"

  app_sample.bar:
    class: App\Sample\SampleBar
    tags:
      - "myapp.sample"

  # ...

  App\SampleResolver:
    class: App\SampleResolver

可読性も向上し、Sampleを追加したり削除したときのメンテも楽になることがわかりますね。

SymfonyのDependencyInjectionで自作のタグを使う方法

便利な自作タグを使う方法を、Symfonyのバージョン別に見ていきましょう。

Symfony2.8でのやり方

サービスにタグを使用する方法 Symfony2日本語ドキュメント にやり方の説明があります。古い記事ですが内容はSymfony2.8でも通用します。

やることは2つだけです。

  • CompilerPassクラスを作る
  • CompilerPassをバンドルに読み込ませる

CompilerPassのサンプル

<?php

namespace Acme\DemoBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class SamplePass implements CompilerPassInterface
{
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        $resolverDefinition = $container->getDefinition('app_sample_resolver');

        foreach ($container->findTaggedServiceIds('myapp.sample') as $id => $tags) {
            $resolverDefinition->addMethodCall('addSample', [new Reference($id)]);
        }
    }
}

CompilerPassをBundleに読み込ませる

<?php

namespace Acme\DemoBundle\AcmeDemoBundle;

use Acme\DemoBundle\DependencyInjection\Compiler\SamplePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeDemoBundle extends Bundle
{
    /**
     * {@inheritdoc}
     */
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new SamplePass());
    }
}

Symfony4でのやり方

Symfony4になっても基本的なやり方は同じです。CompilerPassを読み込ませる対象がBundleではなくKernelに変わっただけです。

  • CompilerPassクラスを作る
  • CompilerPassをKernelに読み込ませる

CompilerPassのサンプル

<?php

namespace App\DependencyInjection\Compiler;

use App\SampleResolver;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class SamplePass implements CompilerPassInterface
{
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        $resolverDefinition = $container->getDefinition(SampleResolver::class);

        foreach ($container->findTaggedServiceIds('myapp.sample') as $id => $tags) {
            $resolverDefinition->addMethodCall('addSample', [new Reference($id)]);
        }
    }
}

CompilerPassをKernelに読み込ませる

<?php
// src/Kernel.php
namespace App;

use App\DependencyInjection\Compiler\SamplePass;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
// ...

class Kernel extends BaseKernel
{
    // ...

    protected function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(new SamplePass());
    }
}

CompilerPassを作らなくてもyaml上でタグ付けられたサービス全てが取得できるようになりました

Symfony3.4以降で、CompilerPassを作ることなくservices.yaml上で 特定のタグが付いたサービス全部 を取得できるようになっています。SampleResolver::addSample() のように、注入するメソッドに渡す追加の引数が特に必要ない場合はこちらのほうが便利だと思います。

# config/services.yaml
services:
  app_sample.foo:
    class: App\Sample\SampleFoo
    tags:
      - "myapp.sample"

  app_sample.bar:
    class: App\Sample\SampleBar
    tags:
      - "myapp.sample"

  # ...

  App\SampleResolver:
    class: App\SampleResolver
    arguments: [!tagged myapp.sample]

まとめ

タグ付けられたサービスをyaml上でまとめて参照できる機能、個人的にはとても便利で嬉しいです。
Symfony2.8→Symfony3.4→Symfony4でDX(DeveloperExperience)が向上しているのを感じます。

参考リンク