PHP製DIコンテナ"yadif"をZend_Applicationと共に使う

Zend_Applicationはフレームワークの設定と主要インスタンスの管理に使いますが、今のところモデル用のリソースがありません。
PHP製のシンプルなDIコンテナ"yadif"は設定によるオブジェクトの生成と管理が可能で、モデルの管理にも適します。ここでは、yadifを利用してサービスリソースを作成し、アプリケーションからモデルにシンボル名でアクセスできるようにします。
Zend_Applicationでのリソースの作成その他については、公式マニュアルに十分な解説がありますが、こちら(Zend_Application (5) 実戦的使い方 - noopな日々)でも書きました。

Yadif - Yet Another Dependency Injection Framework を使う

GitHub - tsmckelvey/yadif: Yet Another (PHP) Dependency Injection Framework
GitHub - beberlei/yadif: Yet Another (PHP) Dependency Injection Framework
yadifは数ファイルで構成されたシンプルなDIフレームワークです。Zend_Applicationと使うときは、yadifの関連ファイルをincludepathに配置して、applicationのiniにautoloadernamespaces[] = Yadifを追加します。*1
yadifでできることを簡単にまとめると

  • オブジェクトを生成し、設定、依存関係を解決します。
    • コンストラクタインジェクション
    • セッターインジェクション
    • factoryオブジェクトを利用した生成
    • 手続きのモジュール化
  • インスタンスをプロパティとして保持

などで、パラメータをアプリケーション上でbindすることも設定ファイルであらかじめbindすることも可能です。
たとえば、Entity_Userの定義で認証後のIdentityをパラメータとして指定しておき、認証後にbindすることでアプリケーション上では生成過程を意識する必要がありません。また、適切なシンボル名(たとえばuser)で利用します。

ビルダーを使う

設定ファイルで作成すると集中管理しやすいですが、ビルダーオブジェクトを使えば、メソッドチェーンでの記述も可能です。

<?php
$builder = new Yadif_Builder();
$builder->bind("InterfaceName")
        ->to("Implementation")
        ->args("ConstructorDependencyA", "ConstructorDependencyB", ":paramA")
        ->param(":paramA", "foo")
        ->method("setFoo")->args("foo");
        ->method("setBar")->args("bar");
        ->scope("singleton");
$config = $builder->finalize();
$yadif = new Yadif_Container($config);

詳しくは、yadif/README.markdown at master · beberlei/yadif · GitHubがわかりやすいです。

コントローラーでコンテナを使う例

DIコンテナを使うと、たとえば、Itemモデルの育成に必要なパラメータやクラス名などをコントローラに書く必要がないので、コントローラーはモデルに何をしてほしいかを書くだけで済みます。モデルの詳細については、設定ファイル上に書きだせるので、コントローラーはモデル(サービス)のインターフェースにのみ依存させることでメンテナンス性が向上します。*2

<?php
interface item
{
    public function process($request);
    public function prepareView();
}
class BagController Extends Zend_Controller_Action
{
    /**
     * model container
     * 
     * @var Yadif_Container
     */
    protected $_container;
    
    /**
     * 
     * @return Yadif_Container
     */
    public function getContainer()
    {
        if (! isset($this->_container)) {
            $this->_container = $this->getInvokeArg('bootstrap')->services;
        }
        return $this->_container;
    }

    public function fooAction()
    {
        $item = $this->getContainer()->item;
        $result = $item->process($this->getRequest())->prepareView();
        $this->view->assign('item', $result);
    }
}

Yadif_Containerをserviceコンテナとしてリソースを作成する

<?php
class Foo_Application_Resource_Services extends Zend_Application_Resource_ResourceAbstract
{
    public function init()
    {
        return new Yadif_Container($this->getOptions());
    }
}

ここでは、Foo_Application_ResourceをPrefixPathsに加えている場合です。ResourceではgetOptionsで設定ファイルの情報を取得できるので、そのままコンテナに抽入して完成です。
これで先ほどのコントローラーでの実装にあるようにservicesでコンテナを参照できます。簡単ですね。

Zend_Applicationでアプリケーションセグメント

Zend_Applicationのリソースはデフォルトでグローバルなスコープと、モジュール別のスコープに分けることができます。同様にシステムをいくつかのセグメントに分割してリソースに管理するような入れ子のリソースを用意すれば、システム内のセグメント毎の設定や挙動の違いを統一された設定ファイルで管理し、モジュールやコントローラーを"安全に"再利用できます。より複雑なウインドウアプリケーションを構築していく場合には頼もしい存在となってくれるように思います。

Yadif_Containerの設定例

上記のようにリソースを書くと、Zend_Applicationの一連の設定ファイルと同じ管理方法でリソース用の設定を書けます。yadifのREADMEにあるような配列が育成されるように作成すると基本的なモデルクラス間の依存性はほとんど設定でかけますし、もし複雑な処理が必要であれば、yadifでは生成用モジュールとfactoryオブジェクトによる生成もサポートしているので、必要なことはほぼすべてできるとみても言い過ぎではないと思います。

resources.services.hoge.class = ArrayObject
resources.services.hoge.arguments.0 = :array
resources.services.hoge.params.:array.a = foo
resources.services.hoge.params.:array.b = bar
resources.services.hoge.scope = prototype

この例では$container->hogeでArrayObjectのインスタンスが取得できます。単純すぎて適切な例ではないかもしれませんね。yadifとZend Frameworkとの連携例は、READMEの中でFrontControllerのカスタマイズ例などが出ています。

リソースでコンテナを使うと何がうれしいか。

READMEでは、Zend_ApplicationにsetContainerする例と、FrontControllerの設定から実行までをサポートする部分までが書いてあります。yadifの使い方が実感できる部分です。
一方、コンテナをZend_Applicationのリソースに割り当てると、グローバルな使い方と違い、モジュールやコントローラーの枠組みを区分して抽象化できます。
アプリケーション内の概念上の別スコープでありながら同じモジュールを使いたいというシナリオではモジュール別ブートストラップ以外に、スコープの違いが表現されなければなりません。こういった問題をコントローラー側でパラメータを解釈して対応するよりも、セットされるコンテナで解決してあればコードやSQLを複雑にせずにすみます。
たとえば、メッセージのポストを扱うコントローラーを書いたら、どのモデルを使用し、対応するビューに何を使うかといった組み合わせをコンテナで解決してスコープ毎にアタッチします。

つまり、リソースを利用することで、モデルを提供するコンテナをスコープ毎に提供することが可能になります。Zend_Applicationの枠組みを大きく外れることなく、複雑な設定を必要とする個別モデル部分で利用でき、Zend Frameworkの手薄なところをうまくカバーできます。

*1:数ファイルなのでincludeするだけでも問題ありません

*2:DIコンテナの利用をつきつめていくとコントローラーもDIで済ませることもできますが、できることと適するかどうかは別途状況判断が必要になる問題