2016年2月25日

プログラミング

SymfonyのバンドルConfigCacheBundleをオープンソースとして公開しました

  • このエントリーをはてなブックマークに追加

マーケティングソリューションカンパニー開発本部UIフロントエンド開発部の三島です。
PHPフレームワークSymfonyで使えるバンドルConfigCacheBundleをオープンソースとして先日公開しました。
公開から少し時間がたちましたが、今回はあらためてこのバンドルの紹介をします。

はじめに

ConfigCacheBundleはPHPフレームワークSymfonyで使えるバンドル(ライブラリ)です。
https://github.com/yahoojapan/ConfigCacheBundle
https://packagist.org/packages/yahoojapan/config-cache-bundle

このバンドルはYAMLやXMLなどの設定ファイルをキャッシュするという機能としてはシンプルなものです。
付加的な機能としてキャッシュ生成前の事前処理や設定ファイルのマージ、バリデーション、多言語対応などを盛り込んでいます。

以下ではConfigCacheBundleの簡単な使い方、機能について説明していきます。またこのバンドルを開発するに至った経緯についても説明します。

使い方

まずSymfonyが動作する環境をご用意ください。セットアップにはSymfonyのドキュメントが参考になります。
Symfonyのバージョンは2.7以上に対応しています。3.0でも動作します。
以下Symfonyがインストールされた状態でConfigCacheBundleのセットアップと使い方の手順を示します。

インストール

ConfigCacheBundleをインストールします。composerを使ってバンドルを入手します。

$ composer require yahoojapan/config-cache-bundle

AppKernel.phpにConfigCacheBundleを追記してバンドルを有効にします。

<?php

// app/AppKernel.php
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        return array(
            //...
            new YahooJapan\ConfigCacheBundle\YahooJapanConfigCacheBundle(),
        );
    }
}

ConfigCacheBundleを使う

Symfonyのバンドルを1つ作成しておきます。

# Symfony 2.x
$ app/console generate:bundle --namespace=Acme/DemoBundle --format=yml
# Symfony 3.x
$ bin/console generate:bundle --namespace=Acme/DemoBundle --format=yml

キャッシュしたい設定ファイルを用意します。

# src/Acme/DemoBundle/Resources/config/sample.yml
invoice: 34843
date   : '2001-01-23'
bill-to:
    given  : Chris
    family : Dumars

次にExtensionBundleの各クラスでキャッシュ生成の準備をします。

<?php

// src/Acme/DemoBundle/DependencyInjection/AcmeDemoExtension.php
namespace Acme\DemoBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use YahooJapan\ConfigCacheBundle\ConfigCache\Register;
use YahooJapan\ConfigCacheBundle\ConfigCache\Resource\FileResource;

class AcmeDemoExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $cache = new Register($this, $container, array(
            // ここでファイルパスと別名を指定します
            new FileResource(__DIR__.'/../Resources/config/sample.yml', null, 'sample'),
        ));
        $cache->register();
    }
}
<?php

// src/Acme/DemoBundle/AcmeDemoBundle.php
namespace Acme\DemoBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeDemoBundle extends Bundle
{
    // boot()を実装してここでキャッシュファイルを生成します
    public function boot()
    {
        $this->container->get('config.acme_demo.sample')->create();
    }
}

この状態でSymfonyのconsoleコマンドを実行するとキャッシュが生成され

# Symfony 2.x
$ app/console cache:warmup
# Symfony 3.x
$ bin/console cache:warmup

キャッシュされた設定ファイルの内容を参照できるようになります。

<?php

// src/Acme/DemoBundle/Controller/WelcomeController.php
namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class WelcomeController extends Controller
{
    public function indexAction()
    {
        // ConfigCache
        $cache = $this->get('config.acme_demo.sample');
        // 34843
        $cache->find('invoice');
        // Chris
        $cache->find('bill-to.given');
    }
}

キャッシュファイルはSymfonyのキャッシュディレクトリ配下にPHPファイルとして生成されます。

// Symfony 2.x : app/cache/dev/acme_demo/75/5b73616d706c655d5b315d.php
// Symfony 3.x : var/cache/dev/acme_demo/75/5b73616d706c655d5b315d.php
<?php return array (
  'lifetime' => 0,
  'data' =>
  array (
    'invoice' => 34843,
    'date' => '2001-01-23',
    'bill-to' =>
    array (
      'given' => 'Chris',
      'family' => 'Dumars',
    )
  )
);

クラスの役割

使い方で登場したConfigCacheBundleの主なクラスの役割を紹介します。

  • Register:SymfonyのサービスコンテナにConfigCacheサービスなどを登録する
  • ConfigCache:設定ファイルのロード、キャッシュ生成、参照を行う
  • FileResource:ファイルパスなどのリソース関連の情報を持つ

diagram

高度な使い方

ConfigCacheBundleでは使い方で示したような基本的な使い方以外にも付加的な機能を用意しています。

キャッシュ生成前の事前処理

設定ファイルの内容を加工してからキャッシュしたいケースはよくあると考えられます。
ConfigCacheBundleではデフォルトで用意してある設定ファイルローダを差し替えることで、設定ファイルのロード後にキャッシュ生成前の事前処理をはさむことができ、内容を加工した状態でキャッシュできます。

事前処理のタイミングは以下のように設定ファイルのロードとキャッシュファイル生成の間です。

flow

設定ファイルのロードはYamlFileLoaderが担います。また事前処理はYamlFileLoaderが内部で保持するArrayLoaderが担います。
ただしデフォルトではYamlFileLoaderArrayLoaderを保持していませんので、キャッシュ生成前の事前処理を行いたいときは

  • YamlFileLoaderArrayLoaderをセットする
  • YamlFileLoaderを別途サービスとして登録する

という手順が必要です。

ローダの差し替え

事前処理をするための実際の例を追ってみます。まずArrayLoaderを実装します。

<?php

// src/Acme/DemoBundle/Loader/ArrayLoader.php
namespace Acme\DemoBundle\Loader;

use YahooJapan\ConfigCacheBundle\ConfigCache\Loader\ArrayLoaderInterface;

class ArrayLoader implements ArrayLoaderInterface
{
    public function load(array $resource)
    {
        $resource['bill-to']['given']  = 'Taro';
        $resource['bill-to']['family'] = 'Yahoo';

        return $resource;
    }
}

これをAcmeDemoBundleでサービスにします。

# src/Acme/DemoBundle/Resources/config/services.yml
services:
    acme_demo.array_loader:
        class: Acme\DemoBundle\Loader\ArrayLoader

    acme_demo.yaml_file_loader:
        class: YahooJapan\ConfigCacheBundle\ConfigCache\Loader\YamlFileLoader
        calls:
            - [addLoader, ['@acme_demo.array_loader']]

Bundle::boot()でキャッシュ生成前にローダを差し替えます。

<?php

// src/Acme/DemoBundle/AcmeDemoBundle.php
namespace Acme\DemoBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeDemoBundle extends Bundle
{
    public function boot()
    {
        $loader = $this->container->get('acme_demo.yaml_file_loader');
        $this->container->get('config.acme_demo.sample')
            ->setLoader($loader)
            ->create()
            ;
    }
}

これでconfig.acme_demo.sampleサービスはArrayLoaderによる置き換え後の配列を持った状態になります。

<?php

// src/Acme/DemoBundle/Controller/WelcomeController.php
namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class WelcomeController extends Controller
{
    public function indexAction()
    {
        $cache = $this->get('config.acme_demo.sample');
        // Taro
        $cache->find('bill-to.given');
    }
}

応用例

前述のサンプルは単純な例ですがTranslatorArrayLoaderを組み合わせて、設定ファイル上の翻訳IDをもとに翻訳済みの文字列にしてキャッシュさせることもできます。
以下のように設定ファイルをもとにlocaleごとのキャッシュファイルを生成するイメージです。

i18n

詳しくは多言語対応のドキュメントに実装例がありますのでそちらもご参照ください。

複数ファイルのマージ、バリデーション

SymfonyのConfigコンポーネントを使って複数ファイルのマージ、バリデーションの機能を実装しています。

以下のように設定ファイルを複数用意して

# src/Acme/DemoBundle/Resources/config/sample1.yml
acme_demo:
    invoice: 34843
    date   : '2001-01-23'
    bill_to:
        given  : Chris
        family : Dumars
# src/Acme/DemoBundle/Resources/config/sample2.yml
acme_demo:
    ship_to:
        given  : Taro
        family : Yahoo

Configurationを作成して

<?php

// src/Acme/DemoBundle/DependencyInjection/Configuration.php
namespace Acme\DemoBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode    = $treeBuilder->root('acme_demo');
        $rootNode
            ->children()
                ->integerNode('invoice')->end()
                ->scalarNode('date')->end()
                ->arrayNode('bill_to')
                    ->children()
                        ->scalarNode('given')->end()
                        ->scalarNode('family')->end()
                    ->end()
                ->end()
                ->arrayNode('ship_to')
                    ->children()
                        ->scalarNode('given')->end()
                        ->scalarNode('family')->end()
                    ->end()
                ->end()
            ->end()
            ;

        return $treeBuilder;
    }
}

Extensionを以下のように記述すると

<?php

// src/Acme/DemoBundle/DependencyInjection/AcmeDemoExtension.php
namespace Acme\DemoBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use YahooJapan\ConfigCacheBundle\ConfigCache\Register;
use YahooJapan\ConfigCacheBundle\ConfigCache\Resource\FileResource;

class AcmeDemoExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $cache = new Register($this, $container, array(
            new FileResource(__DIR__.'/../Resources/config/sample1.yml'),
            new FileResource(__DIR__.'/../Resources/config/sample2.yml'),
        ));
        $cache->register();
    }
}

マージされた内容を参照できるようになります。

<?php

// src/Acme/DemoBundle/Controller/WelcomeController.php
namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class WelcomeController extends Controller
{
    public function indexAction()
    {
        $cache = $this->get('config.acme_demo');
        // Chris
        $cache->find('bill_to.given');
        // Taro
        $cache->find('ship_to.given');
    }
}

設定ファイルのバリデーションはConfigurationで定義された内容が適用されます。
設定ファイルのマージを許可する/しないの指定や値の型指定などができます。
Configurationは本来はアプリケーション設定app/config/config.ymlをSymfonyのバンドル上で参照する際に、設定値をバリデーションするために用意されたものですが、それを通常の設定ファイルにも適用できるようにしています。

ConfigurationについてはSymfonyのドキュメントに詳細が記載されていますのでそちらをご参照ください。

開発の経緯

広告管理ツールの刷新

私が所属しているチームではプロモーション広告の広告管理ツール(下図)のフロントエンドの開発、保守を行っています。

ss

広告管理ツールは2011年にリリースされたもので、今でも現役で稼働しています。リリースからこれまでの間さまざまな機能追加がされていますので内部のコードはかなり複雑になっています。またコード自体古くレガシーになりコードのメンテナンスにコストがかかる状態になっていました。
このような問題を解消するために1年ほど前から刷新プロジェクトが立ち上がり、現行システムを廃止して基盤プラットフォームをのせかえる対応に取り組みはじめました。
新しい基盤プラットフォームではサーバーサイドのフレームーワークとしてSymfony2を採用し、今回公開したConfigCacheBundleはこのプラットフォームの一部として開発されました。ConfigCacheBundleは広告管理ツールの新しいプラットフォームで実際に稼働しはじめています。

開発の理由

ConfigCacheBundleを開発した理由は単純でSymfony上で設定ファイルをキャッシュするための柔軟な仕組みがなかったためです。

Symfonyにはパラメータがありこれを用いて設定ファイルキャッシュを実現する方法も考えられますが、以下のような扱いづらいケースが出てきます。

  • 事前処理をするためにサービスが使えない
    パラメータに対して事前処理をはさむタイミングとしてはExtension::load()またはCompilerPass::process()があります。しかしExtension::load() CompilerPass::process()ではサービスコンテナが未完成のためサービスを自由に使えない制約があります。
    サービスを差し替えて事前処理の挙動を柔軟に変更したかったのでパラメータでは対応しづらい問題がありました。
     
  • メモリ消費の問題
    パラメータはSymfonyのサービスコンテナの中に直接定義されるためパラメータの容量はそのままサービスコンテナにも加わることになります。ファイル数が多くなったり容量の大きいファイルをパラメータとして定義するとサービスコンテナも大きくなります。Symfonyを使う際はサービスコンテナは必ずロードされるため、使わない設定であってもメモリに置かれることになり結果的に無駄なメモリを消費してしまいます。
    広告管理ツールでは容量の大きい設定ファイルを扱うケースもありましたのでパラメータとは別で管理しようと考えました。

また応用例で少し触れましたが、設定ファイル上の翻訳IDを事前に翻訳してキャッシュしたいという目的がありました。広告管理ツールでは日本語/英語の多言語対応をするために、設定ファイルに翻訳IDを持っておいてそれを翻訳するケースがよくあります。現行システムではリクエストのたびに都度翻訳をしていましたが、これを事前に翻訳しておいてキャッシュできれば都度翻訳は不要ですので処理速度を速めることができます。そのため事前処理のしやすい設定ファイルキャッシュの仕組みが必要でした。

まとめ

今回はConfigCacheBundleの紹介として簡単な使い方、機能、開発の経緯について説明しました。
Symfonyを使っている開発者にとって役に立つバンドルですのでぜひ使ってみてください。
ご意見やご要望、バグ修正など随時受け付けていますのでgithubでご報告いただければと思います。

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加