@hidenorigotoです。 1/24日のSymfony Meetupで、「LisketでのSymfony活用例紹介」と題して、Symfonyの機能をやや高度に利用する例を紹介するLTをしてきました。このような使い方の例を今後このブログで紹介するとともに、Symfonyの内部のしくみを探ってみようと思います。

今回はLTで発表したうちの1つ目のものを、少し詳しく解説します。

連想配列のデータ

アプリケーションで、ツリー構造のデータ定義などを連想配列で扱いたいことがあります。カルテットで開発しているLisketでは、各ユーザーの契約状態によって利用可能なツールセットが変わりますが、そのような状態からは独立して、ベースとなるメニューツリーの構造定義を持っています。

階層を持ったデータを記述できるフォーマットはいくつかありますが、Symfonyには標準でYAMLコンポーネントがありますので、これを使います。たとえば次のような定義を使いたいとします。

menu_category1:
    label: メニューカテゴリ1
    children:
        toolA:
            label: ツールA
        toolB:
            label: ツールB
menu_category2:
    label: メニューカテゴリ2
    children:
        toolC:
            label: ツールC
        toolD:
            label: ツールD

YAMLコンポーネントを使えば、特定のパスにあるYAMLファイルを読み込んで、連想配列として扱うこと自体はとても簡単です。しかし、アプリケーションでは次のようなことも考慮しなくてはなりません。

  • 小さなファイルとはいえ、デプロイ後に変更されることがない定義ファイルを、リクエストのたびに読み込む処理が発生するのは無駄なので、キャッシュしたい
  • 複数のクラスから定義データを使えるようにしたい

このようなニーズも、Symfonyのサービスコンテナが持つ仕組みに乗せればとてもシンプルに解決できます。

サービスコンテナを使って定義データを読み込む

Symfonyのサービスコンテナでは、サービスの他に、パラメータを扱うことができます。パラメータはコンテナ内に静的に格納されるkey-value形式のデータで、Symfonyのコンソールコマンド debug:container--parameters オプション付きで実行すると、現在のサービスコンテナに格納されているパラメータの一覧を確認できます。

$ php app/console debug:container --parameters
[container] List of parameters
 Parameter                                     Value
 :
 kernel.cache_dir                              /path/to/app/cache/dev
 kernel.charset                                UTF-8
 :

先ほどのYAMLファイルを、サービスコンテナのパラメータとして扱えるようにするには、全体をparameters: 以下に記述するようにし、さらにこの定義データ全体のキー名(menu.tree)を割り当てて次のようにします。

parameters:
    menu.tree:
        menu_category1:
            label: メニューカテゴリ1
            children:
                toolA:
                    label: ツールA
                toolB:
                    label: ツールB
        menu_category2:
            label: メニューカテゴリ2
            children:
                toolC:
                    label: ツールC
                toolD:
                    label: ツールD

定義ファイル側の準備はこれだけです。このファイルをバンドルの Resources/config ディレクトリに menu.yml という名前で配置しておき、バンドルのエクステンションで読み込むようにします。

<?php
namespace AppBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader;

class AppExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('menu.yml');
    }
}

SymfonyのドキュメントHow to Load Service Configuration inside a Bundleも合わせて参照してください。

これだけで、サービスコンテナに menu.tree という名前のパラメータが格納されます。使いたい個所で $container->getParameter('menu.tree') のようにして、パラメータを取り出して使えます。

今回作成したファイル:
image

サービスコンテナの中身

Symfonyのサービスコンテナは、様々な定義からコンパイルされた結果が1つのPHPクラスとして生成され、アプリケーションのキャッシュディレクトリに保存されています。dev環境の場合は、appDevDebugProjectContainer.php というクラスになっています。パラメータは、このクラス内の getDefaultParameters() メソッドが返す連想配列として静的に定義されています。先ほどYAMLで定義した menu.tree も、連想配列として追加されていることが分かります。

(appDevDebugProjectContainer.php内)

/**
 * Gets the default parameters.
 *
 * @return array An array of the default parameters
 */
protected function getDefaultParameters()
{
    return array(
        'kernel.root_dir' => ($this->targetDirs[3].'/app'),
        :
        'menu.tree' => array(
            'menu_category1' => array(
                'label' => 'メニューカテゴリ1',
                'children' => array(
                    'toolA' => array(
                        'label' => 'ツールA',
                    ),
                    'toolB' => array(
                        'label' => 'ツールB',
                    ),
                ),
            ),
            'menu_category2' => array(
                'label' => 'メニューカテゴリ2',
                'children' => array(
                    'toolC' => array(
                        'label' => 'ツールC',
                    ),
                    'toolD' => array(
                        'label' => 'ツールD',
                    ),
                ),
            ),
        ),

サービスにパラメータをDI

サービスコンテナの持つパラメータは、サービスと同様に、DIして使えます。パラメータを使いたい場所がコントローラではなく独立したクラスの場合は、この方法を使います。たとえばメニューのための何らかの処理を行うクラスが次のようだったとします。

(MenuHandler.php)

namespace AppBundle;

class MenuHandler
{
    /**
     * @var array
     */
    private $menuTree;

    /**
     * @param array $menuTree
     */
    public function __construct($menuTree)
    {
        $this->menuTree = $menuTree;
    }
}

このクラスのコンストラクタで menu.tree パラメータをインジェクトしたいので、先ほどの menu.yml ファイルに次のようにサービス定義を追加します。

services:
    app.menu_handler:
        class: AppBundle\MenuHandler
        arguments: ['%menu.tree%']

これで、app.menu_handler サービスを取得したときに menu.tree のデータがコンストラクタの引数でインジェクトされ、自由に利用できるようになります。

サービスコンテナの中身

サービス定義は、1つずつサービスコンテナのメソッドにコンパイルされます。今回定義した app.menu_handler サービスがどのようなコードにコンパイルされているのか appDevDebugProjectContainer.php を見てみましょう。ファイル内を app.menu_handler で検索すると、次のようなメソッドが見つかります。

/**
 * Gets the 'app.menu_handler' service.
 *
 * This service is shared.
 * This method always returns the same instance of the service.
 *
 * @return \AppBundle\MenuHandler A AppBundle\MenuHandler instance.
 */
protected function getApp_MenuHandlerService()
{
    return $this->services['app.menu_handler'] = new \AppBundle\MenuHandler(array('menu_category1' => array('label' => 'メニューカテゴリ1', 'children' => array('toolA' => array('label' => 'ツールA'), 'toolB' => array('label' => 'ツールB'))), 'menu_category2' => array('label' => 'メニューカテゴリ2', 'children' => array('toolC' => array('label' => 'ツールC'), 'toolD' => array('label' => 'ツールD')))));
}

これを見ると、MenuHandlerクラスをインスタンス化しているのは分かりますが、パラメータについては getDefaultParameters() 等を経由して取得するのではなく、値がインラインで埋め込まれています。パラメータの取得といえど、アプリケーションの規模によっては全体で何千回、何万回と呼び出されることになるので、サービスコンテナへコンパイルする時点で決定しているものは、このようにインライン化して高速化していることが分かります。

まとめ

階層を持った連想配列データを扱うという、割と簡単な問題の解法から、Symfonyのサービスコンテナの中身を見てみました。Symfonyのサービスコンテナは、定義ファイルなどにより十分うまく抽象化されているため、素朴に利用するだけであればサービスコンテナのコードまで追う必要はありません。一方で、エクステンションやコンパイラといった機能を備えているのがSymfonyのサービスコンテナの魅力でもあります。これらの機能の働きを知り使いこなす上では、コンパイル結果のコードを知るのが一番の近道です。Symfonyを使いこなせるようになりたい方は、是非一度はサービスコンテナのコードを見ておくことをお薦めします。

参考

拙著「基本からしっかり学ぶSymfony2入門」でも、サービスコンテナの機能について解説しています。