ZendFrameworkでモデルをメソッドチェーンで呼ぶ様にしてみた

モデルの使い方も人それぞれだと思うのですが、個人的にメソッドチェーンが好きなので、とりあえずその様に扱える様にしてみました。

実用的かどうかは実用してみないと分かりません。

使用バージョン:Ver1.7.7

http://framework.zend.com/download/current/ ZFのダウンロードはこちら

どうやってやろうか?

とりあえずメソッドチェーンでググッてみる。

一行でカタをつけるにはコンストラクタからメソッドチェーンする方法くらいしかないらしい。
PHPでコンストラクタからメソッドチェーンする方法 - id:anatooのブログ


OpenPEARPHP_Objectというライブラリもある。個人的に注目してます。
http://openpear.org/package/PHP_Object


一応今回の目的としては、ZendFramework内で用意したModelクラスを使う際に、

<?php

    $model = new Foo;
    $result = $model->var();
    $result = $model->baz();

等と面倒な事をしないで、

<?php

    $result = $this->Foo()->var()->baz();

みたいにワンライナーでやれたら気持ちいいしOOP的に何か発見があるかもというところ。

呼び出し側のコントローラーと、呼び出すモデルの定義

とりあえず、

<?php

class IndexController extends Zend_Controller_Action_Chain
{

    public function indexAction()
    {

        $this->Account()->set('taketin')->say('hello');
        exit;
    }

}

等と書いたIndexControllerを用意した。
これから拡張するZend_Controller_Actionの拡張クラス名は、Zend_Controller_Action_Chainとした。

$this->Account()->set('taketin')->say('hello');

ここでやりたい事は、AccountというModelクラスをインスタンス化して、任意のアカウントをセットし、任意の文字列を表示させるという感じ。実験用にありがちな感じ。


実際にこのModelクラスを定義する。

<?php

/**
 * メソッドチェーンテスト用モデル
 */
class Account
{

    private $_account;
    
    public function __construct()
    {
        return $this;
    }

    public function set($account) {
        $this->_account = $account;
        return $this;
    }

    public function say($message) {
        echo $this->_account . ' say ' . $message;
        return $this;
    }
    
}

ちなみにModel階層までのinclude_pathを通しておく事と、クラス名 = ファイル名になっている事が前提条件となる。
なので、このテスト用クラスをAccount.phpとして保存しておく。

次に、Zend_Controller_Actionを拡張していく。

Zend_Controller_Action を継承する

$this->(クラス名)

と呼び出したいので、存在しないメソッドをコールした時に呼ばれるマジックメソッドである、__call()をオーバーライドしたい。
Zend_Controller_Action内を見てみる。

<?php

    public function __call($methodName, $args)
    {
        require_once 'Zend/Controller/Action/Exception.php';
        if ('Action' == substr($methodName, -6)) {
            $action = substr($methodName, 0, strlen($methodName) - 6);
            throw new Zend_Controller_Action_Exception(sprintf('Action "%s" does not exist and was not trapped in __call()', $action), 404);
        }

        throw new Zend_Controller_Action_Exception(sprintf('Method "%s" does not exist and was not trapped in __call()', $methodName), 500);
    }

あったあった。
やってる事は、存在しないアクションメソッドをコールされた時にエラーコード404で例外をスローして、その他の存在しないメソッドの場合はエラーコード500で例外をスローする、というもの。


ここにModelの呼び出しを突っ込んでやればいい。

<?php

/**
 * Zend_Controller_Action拡張クラス その1
 */
class Zend_Controller_Action_Chain extends Zend_Controller_Action
{

    /**
     * __callをオーバーライドする
     */
    public function __call($methodName, $args)
    {
        require_once 'Zend/Controller/Action/Exception.php';
        if ('Action' == substr($methodName, -6)) {
            $action = substr($methodName, 0, strlen($methodName) - 6);
            throw new Zend_Controller_Action_Exception(sprintf('Action "%s" does not exist and was not trapped in __call()', $action), 404);
        }

        try {
            /**
             * モデルを呼び出す
             */
            require_once($methodName . '.php');
            $model = new $methodName;
            return $model;
        } catch (Zend_Controller_Action_Exception $e) {
            /**
             * テストの為、例外が発生したらメッセージ表示させてexit
             */
            echo $e->getMessage();exit;

            throw new Zend_Controller_Action_Exception(sprintf('Method "%s" does not exist and was not trapped in __call()', $methodName), 500);
        }

    }

}

try〜catch構文の中にモデル呼び出しを仕込む事で、存在しないモデルがコールされた場合は従来通り例外をスローする作り。これだと既存の作りにも影響しない(はず)。
これでとりあえず実行してみる。

taketin say hello

と表示された。
よし、成功。

仕掛けたtry〜catch構文がきちんと効いているか確認する。

IndexController.php

$this->Account()->set('taketin')->say('hello');

をわざと

$this->Hoge()->set('taketin')->say('hello');

等と存在しないModel名にして再実行。
これで例外がスローされて、echo $e->getMessage();exit; が実行されれば成功。


しかし…

Fatal error: Zend_Controller_Action_Chain::__call() [function.require]: Failed opening required 'Hoge.php'

あれ、FatalErrorが出た。

ここでPHPの例外処理について再調査。どうもFatalErrorは捕捉できないらしい。
(致命的なエラーはその時点でプログラム終了するから。当然といえば当然ですね。)
さらに、致命的なエラー以外のエラーが発生した場合のハンドリング方法の指定も必要らしい。

方法としては、

set_error_handler

という標準関数を使うといいらしい。
set_error_handlerマニュアル


ここで色々と調べて、FatalErrorを出さずに同じ事を行う方法は以下の様になった。

<?php

/**
 * Zend_Controller_Action拡張クラス その2
 */
class Zend_Controller_Action_Chain extends Zend_Controller_Action
{

    /**
     * コールされたモデルが存在しなかった場合、例外をスロー
     */
    public function errorHandler($errno, $errstr, $errfile, $errline) {
        throw new Zend_Controller_Action_Exception($errstr, $errno);
    }

    /**
     * __callをオーバーライドする
     */
    public function __call($methodName, $args)
    {
        require_once 'Zend/Controller/Action/Exception.php';
        if ('Action' == substr($methodName, -6)) {
            $action = substr($methodName, 0, strlen($methodName) - 6);
            throw new Zend_Controller_Action_Exception(sprintf('Action "%s" does not exist and was not trapped in __call()', $action), 404);
        }

        set_error_handler(array($this, 'errorHandler'));

        try {
            /**
             * モデルを呼び出す
             */
            include_once($methodName . '.php');
        } catch (Zend_Controller_Action_Exception $e) {
            /**
             * テストの為、例外が発生したらメッセージ表示させてexit
             */
            echo $e->getMessage();exit;

            throw new Zend_Controller_Action_Exception(sprintf('Method "%s" does not exist and was not trapped in __call()', $methodName), 500);
        }

        $model = new $methodName;
        return $model;

    }

}

ポイントは、require_onceをやめてinclude_onceにした事と、set_error_handler()と、そのコールバックメソッドであるerrorHandler()の定義。これで、notice, warning時のエラー捕捉を行える。


include_onceはファイルが読み込めなかった場合warningエラーをスローするので、ここでset_error_handlerに設定されたコールバックメソッドであるerrorHandler()へと制御が移る。


errorHandler()内ではZend_Controller_Action_Exceptionで例外をスローさせて、結果的にtry〜catch構文でwarningエラー時の例外捕捉が可能となった。

Singletonパターンにしてみる

これで目的はほぼ達成されたものの、毎回

$this->Account()

の度にインスタンス生成されたのではコストもかかるし、プロパティに情報を残しておく事もできない。(使い捨てオブジェクトは別として)


なのでSingletonパターンぽくして、インスタンスは一個しか生成しないようにしてみる。

<?php

/**
 * Zend_Controller_Action拡張クラス その3(一応完成)
 */
class Zend_Controller_Action_Chain extends Zend_Controller_Action
{

    /**
     * インスタンス化したモデルを格納する
     * (SingletonPattern)
     */
    protected $_model = array();

    /**
     * コールされたモデルが存在しなかった場合、例外をスロー
     */
    public function errorHandler($errno, $errstr, $errfile, $errline) {
        throw new Zend_Controller_Action_Exception($errstr, $errno);
    }

    /**
     * __callをオーバーライドする
     */
    public function __call($methodName, $args)
    {
        require_once 'Zend/Controller/Action/Exception.php';
        if ('Action' == substr($methodName, -6)) {
            $action = substr($methodName, 0, strlen($methodName) - 6);
            throw new Zend_Controller_Action_Exception(sprintf('Action "%s" does not exist and was not trapped in __call()', $action), 404);
        }

        set_error_handler(array($this, 'errorHandler'));

        try {
            /**
             * モデルを呼び出す
             */
            include_once($methodName . '.php');
        } catch (Zend_Controller_Action_Exception $e) {
            throw new Zend_Controller_Action_Exception(sprintf('Method "%s" does not exist and was not trapped in __call()', $methodName), 500);
        }

        /**
         * 初回のみインスタンス生成しプロパティに保持しておく
         */
        if (!isset($this->_model[$methodName])) {
            $this->_model[$methodName] = new $methodName;
        }
            
        return $this->_model[$methodName];

    }

}

これで、一回のリクエスト内で複数回

$this->Account()

と呼び出してもインスタンス生成するのは最初だけとなった。
逆にSingletonパターンにしたくないModelクラスの場合は別途処理が必要となるが。。。

イメージとしては

$this->trash()->Account()

とかで、明示的に使い捨てである事を宣言させるとか?
こんな仕様も__call()メソッドに処理書けばできますね。


とりあえず今回はここまで。