ZendFrameworkでモデルをメソッドチェーンで呼ぶ様にしてみた
モデルの使い方も人それぞれだと思うのですが、個人的にメソッドチェーンが好きなので、とりあえずその様に扱える様にしてみました。
実用的かどうかは実用してみないと分かりません。
どうやってやろうか?
とりあえずメソッドチェーンでググッてみる。一行でカタをつけるにはコンストラクタからメソッドチェーンする方法くらいしかないらしい。
PHPでコンストラクタからメソッドチェーンする方法 - id:anatooのブログ
OpenPEARのPHP_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();
呼び出し側のコントローラーと、呼び出すモデルの定義
とりあえず、<?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()メソッドに処理書けばできますね。
とりあえず今回はここまで。