DIコンテナの起動とエクステンション

この記事は、Symfony アドベントカレンダー 2010 に参加しています。


DIコンテナの起動処理を簡単にまとめます。DIコンテナがどういったものかわかっており、Symfony2のバンドルやカーネルといった名称と役割が何となくわかっている方が対象です。

そもそもDIコンテナの起動とは、DIコンテナにパラメーター定義とサービス定義が行われる処理をさします。最初におおまかな流れを見てみましょう。

  1. KernelがDIコンテナオブジェクトを生成
  2. すべてのバンドルから エクステンション を抜き出してDIコンテナに登録
  3. app/config/config.ymlを読み込み、内容に応じてエクステンションをロード

DIコンテナのにパラメーターやサービスを定義する、つまりDIコンテナを拡張する処理は、DependencyInjectionコンポーネントに含まれるエクステンションクラスを用いて行います。
それではDIコンテナがエクステンションによって拡張される流れを細かく見ていきましょう。

KernelがDIコンテナオブジェクトを生成

Symfony\Component\HttpKernel\Kernelクラスが最初にリクエストを処理する際、Kernelクラスのboot()メソッドが読み込まれます。boot()メソッドはKernelに登録されたバンドルを一斉に読み込み、それらの中からエクステンションを抜き出して、DIコンテナを起動する処理を行います。バンドルの読み込みとDIコンテナの起動がboot()メソッドの役割です。

<?php

class Kernel
{
    public function boot()
    {
        // ...

        $this->bundles = $this->registerBundles();
        $this->bundleDirs = $this->registerBundleDirs();
        $this->container = $this->initializeContainer();

        // ...

        foreach ($this->bundles as $bundle) {
            $bundle->setContainer($this->container);
            $bundle->boot();
        }

        $this->booted = true;
    }
}

registerBundles()はバンドルオブジェクトの配列を返すメソッドで、Kernelクラスには抽象メソッドとして定義されています。実際のアプリケーションのKernelで必要なバンドルオブジェクトを返すように設定し、それが$this->bundlesプロパティにそのまま格納されます。registerBundleDirs()も大した処理はしてないのでいいでしょう。

そのすぐ後に呼び出されるのがinitializeContainer()です。initializeContainer()は見てわかる通りDIコンテナの初期化メソッドです。DIコンテナオブジェクトの起動が完了したら、起動後の状態と同じ状態のパラメーターやサービス情報が静的に定義されたクラスファイルをキャッシュとして生成します。initializeContainer()メソッドではそのキャッシュの制御を行い、キャッシュが存在しない場合など、バンドルからコンテナの情報を読み込む必要がある場合は buildContainer() メソッドを呼び出します。

<?php

class Kernel
{
    protected function buildContainer($class, $file)
    {
        $parameterBag = new ParameterBag($this->getKernelParameters());

        $container = new ContainerBuilder($parameterBag);
        foreach ($this->bundles as $bundle) {
            $bundle->registerExtensions($container);

            if ($this->debug) {
                $container->addObjectResource($bundle);
            }
        }

        if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) {
            $container->merge($cont);
        }
        $container->freeze();

        // ...
    }
}

buildContainer()メソッドではContainerBuilderオブジェクトを新規に作成します。これは空っぽのDIコンテナオブジェクトです。ここにサービスなどを登録していきます。

すべてのバンドルからエクステンションを抜き出してDIコンテナに登録

上記のbuildContainer()メソッドを見ると、$this->bundlesをループして、すべてのバンドルのregisterExtensions()メソッドにコンテナオブジェクトを渡しています。このメソッドは、各バンドルディレクトリの中 DependencyInjection ディレクトリの中から Extension.php で終わるファイルを検索し、それらを読み込みます。ここで探して読み込むのがエクステンションです。

エクステンションは Symfony\Component\DependencyInjection\Extension\Extension クラスを継承したクラスです。作成する場合は前述の読み込み規則に従い、各バンドルのDependencyInjectionディレクトリ内にExtension.phpで終わるファイルを作成し、その中にクラスを定義します。

読み込む際には実際にエクステンションのインスタンスを作成します。それらのインスタンス1つ1つをコンテナオブジェクトに登録します。コンテナへの登録は、ContainerBuilder::registerExtension()メソッドを通じて行います。これはBundle::registerExtensions()メソッドの内部で呼び出されます。

コンテナに登録した段階では、パラメーターやサービスの定義はコンテナーには追加されません。この段階では単にコンテナーオブジェクトの内部にエクステンションオブジェクトが保持されるだけです。

<?php

class ContainerBuilder
{
    static public function registerExtension(ExtensionInterface $extension)
    {
        static::$extensions[$extension->getAlias()] = static::$extensions[$extension->getNamespace()] = $extension;
    }
}

このとき覚えておいて欲しいのが、$extensionsプロパティに格納される際に、Extension::getAlias()メソッドの戻り値をキーとして使用しています。これは後ほど非常に重要な役割を持ちます。FrameworkBundleのFrameworkExtensionでは 'app' という文字列をエイリアスとして返すようになっています。

app/config/config.ymlを読み込み、内容に応じてエクステンションをロード

すべてのエクステンションがContainerに登録されたら、次は各エクステンションをロードする処理です。各エクステンションにはそれぞれ独自にDIコンテナを拡張するためのメソッドを持っています。たとえばFrameworkBundleのFrameworkExtensionではconfigLoad()メソッドがそれです。

実際にロードを行う処理は、config.ymlが密接に関係しています。たとえばconfig.ymlには次のような記述があります。

app.config:
    charset:       UTF-8
    error_handler: null
    csrf_secret:   xxxxxxxxxx
    router:        { resource: "%kernel.root_dir%/config/routing.yml" }
    validation:    { enabled: true, annotations: true }
    templating:
        escaping:       htmlspecialchars

ここには「app.config」という名前でさまざまな設定が定義されています。実は、この「app.config」という一文によってエクステンションがロードされます。

エクステンションの登録が完了された後、このconfig.ymlが読み込まれます。読み込みはKernel:: registerContainerConfiguration()メソッドで行われます。その際、(app.configなどの)トップレベルの項目が、それぞれエクステンションをロードするためのキーになります。

たとえばapp.configの場合、エクステンションの中から app という名前で登録されているものを探し、そのエクステンションの configLoad() メソッドを呼び出します。トップレベルの項目すべてにそれを行います。

エクステンションの名前は、各エクステンションクラスのgetAlias()メソッドの戻り値です。呼び出されるメソッドは、ドットの後に指定した文字列にLoadをつけたメソッドです。いくつかの例を記述します。

  • foo.bar: fooエクステンションの barLoad()
  • app.test: appエクステンションの testLoad()

あとはそれぞれのエクステンションのメソッドに、コンテナオブジェクトと、config.yml側に記述してある設定項目の連想配列が渡されます。

バンドルではResources/configディレクトリにDIコンテナを拡張するための定義ファイルを配置する習慣があります。それは読み込むだけでDIコンテナのサービスやパラメータ定義を拡張します。

config.ymlに記述してある内容はそれとは違います。config.ymlはユーザーが必要な分だけDIコンテナへの拡張を設定するためのファイルです。ここに記述した設定を受け取り、エクステンションが内部でうまいことコンテナオブジェクトを修正してくれます。

単にエクステンションを読み込みたいだけなら、次のようにチルダーを指定します。

markdown.parser: ~

チルダーはnullと同じです。これでも問題なく動作します。これがないと、上記の場合はmarkdownエクステンションがそもそも読み込まれません。エクステンションをとりあえず有効にするためにはこういった記述が必要になります。


さて、ここまでが起動の流れになります。自前でバンドルを作った際にエクステンションも同時に作る場合、必ずconfig.yml側で読み込んであげる必要があります。

とりあえず習慣的には、 'ユニークなエイリアス.config' というのが基本のようです。とりあえずここまでの流れが追えるとだいぶSymfony2がわかった気になりますよ!

次はid:uechocoさん、よろしくお願いします!



Symfony Advent 2010であなたの記事を公開してみませんか?

Symfony Advent 2010では12月1日から12月24日までを使って日替わりでsymfonyでイイなと思った小さなtipsから内部構造まで迫った解説などをブログ記事にして公開していくイベントです。

参加についてはATNDで参加表明の上、Google GroupのSymfony Advent 2010に追加リクエストを送信ください。

Symfony Advent 2010チーム一同、あなたの参加をお待ちしております。