Zend_ExceptionとErrorHandlerプラグインとErrorControllerでのエラーハンドリングについて

例外ハンドリング時に考えた色々な事についての備忘録。

使用バージョン:Ver1.7.8

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

ZFにおける例外発生時の処理

ZFはPHP5ベースなので、何か問題が発生した時は例外をスローして対応する仕様となってます。
デフォルトの状態では例外がスローされると、Zend_Controller_Plugin_ErrorHandler.phpを経由してErrorController.phpに処理が委譲されます。


ErrorController.phpの基本的な書式はZFのマニュアルページにあります。
http://framework.zend.com/manual/ja/zend.controller.plugins.html

具体的には以下のような感じです。

<?php

class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                // 404 エラー -- コントローラあるいはアクションが見つかりません
                $this->getResponse()->setRawHeader('HTTP/1.1 404 Not Found');

                $content =<<<EOH
<h1>エラー!</h1>
<p>そのページは存在しません。</p>
EOH;
                break;
            default:
                // アプリケーションのエラー
                $content =<<<EOH
<h1>エラー!</h1>
<p>予期せぬエラーが発生しました。後でもう一度お試しください。</p>
EOH;
                break;
        }

        // 前回の内容を消去します
        $this->getResponse()->clearBody();

        $this->view->content = $content;
    }
}

ErrorHandlerプラグインから受け取った、エラーオブジェクトである$errorsが持つ、typeプロパティに保存されているエラータイプ文字列によってswich文で処理を分岐しています。
caseが3つです。
ここに書いてあるのは、
「要求されたcontroller名かaction名が存在していなかった時の処理」と、「それ以外」という大胆な分け方なので、「それ以外」をもっと細分化できれば、例外の種類によって細かな処理の記述ができるんじゃ?と、思いました。

とりあえず、ZFのコードを見てみる事に。

Zend_Controller_Plugin_ErrorHandler.php

ソースを見てみると、頭の方で先ほどのErrorController.php内のswitch条件になっていた、エラータイプ文字列が定数として定義されている。

<php

    /**
     * Const - No controller exception; controller does not exist
     */
    const EXCEPTION_NO_CONTROLLER = 'EXCEPTION_NO_CONTROLLER';

    /**
     * Const - No action exception; controller exists, but action does not
     */
    const EXCEPTION_NO_ACTION = 'EXCEPTION_NO_ACTION';

    /**
     * Const - Other Exception; exceptions thrown by application controllers
     */
    const EXCEPTION_OTHER = 'EXCEPTION_OTHER';

こんな感じに3種類。

ソースを下っていくと、最終的にpostDispatch内でswitchによる処理分岐が行われている。

<?php

            // Get exception information
            $error            = new ArrayObject(array(), ArrayObject::ARRAY_AS_PROPS);
            $exceptions       = $response->getException();
            $exception        = $exceptions[0];
            $exceptionType    = get_class($exception);
            $error->exception = $exception;
            switch ($exceptionType) {
                case 'Zend_Controller_Dispatcher_Exception':
                    $error->type = self::EXCEPTION_NO_CONTROLLER;
                    break;
                case 'Zend_Controller_Action_Exception':
                    if (404 == $exception->getCode()) {
                        $error->type = self::EXCEPTION_NO_ACTION;
                    } else {
                        $error->type = self::EXCEPTION_OTHER;
                    }
                    break;
                default:
                    $error->type = self::EXCEPTION_OTHER;
                    break;
            }

ここでやってる事を上から順に見ると、

$errors変数に、SPLオブジェクトであるarrayObjectを作成。
ZFのレスポンスオブジェクトに保存してあるスローされた例外を納めた配列を取得。
その中から、一番直近の例外を取得。
一番直近の例外クラス名を取得。
$errorsに一番直近の例外クラスを格納。
switchにより、例外クラス名で処理分岐し、冒頭で設定していた定数をエラータイプとして$errorsに格納している。


…と、いう事は、

1. 例外クラスを新たに作る。
2. ErrorHandlerに、1で作った例外クラスに対応した定数を作る。
3. ErrorControllerのswitch分岐に、2で作った定数に対応した処理を入れる。

の3stepで例外ハンドリングの細分化ができそうである。

例外クラスを新たに作る

そもそもZFの基本例外クラスであるZend_Exceptionはどういうクラスなのか。
ソースを見てみる。

<?php

class Zend_Exception extends Exception
{}

…うお、何も書いてない。
SPLで定義されている例外基本クラスであるExceptionを継承しているが、処理のオーバーライドは無し。
他に定義されているExceptinクラスもいくつか見てみたが、Zend_Exceptionを継承していたりするクラスもほぼ全て実処理は何も書いてなかった。

と、いう事は、この行為は「単なる名義作り」という事が想定される。
ここでそもそもExceptionクラスとは…みたいな事を少し調べてみた。詳細はマニュアルを見るべし。
http://php.benscom.com/manual/ja/class.exception.php
あとPHPマニュアルのSPL編にはこんなページもあったんだな、という発見の喜びも込めて、SPLで用意されている例外クラス群は以下。
PHP: Manual Quick Reference


というわけで、適当な例外クラスを作ってみる。

<?php

class Zend_Original_Exception extends Zend_Exception
{}

ErrorHandlerプラグインを拡張する

次にErrorHandler.phpを継承した拡張クラスを作る。

<?php

class Zend_Controller_Plugin_Custom_ErrorHandler extends Zend_Controller_Plugin_ErrorHandler
    /**
     * 独自に設定したエラーのハンドリング用
     */
    const EXCEPTION_ORIGINAL = 'EXCEPTION_ORIGINAL';

    /**
     * switchにcase追加
     */
    public function postDispatch(Zend_Controller_Request_Abstract $request)
    {
      〜省略

            switch ($exceptionType) {
                case 'Zend_Controller_Dispatcher_Exception':
                    $error->type = self::EXCEPTION_NO_CONTROLLER;
                    break;
                case 'Zend_Controller_Action_Exception':
                    if (404 == $exception->getCode()) {
                        $error->type = self::EXCEPTION_NO_ACTION;
                    } else {
                        $error->type = self::EXCEPTION_OTHER;
                    }
                    break;
         /**
                 * 先ほど作成した例外クラスの名前でcase追加
                 */
                case 'Zend_Original_Exception':
                    $error->type = self::EXCEPTION_ORIGINAL;
                    break;
                default:
                    $error->type = self::EXCEPTION_OTHER;
                    break;
            }

      省略〜
    }

ErrorControllerに処理を追加する。

例外がスローされた時に発生に行う処理を追加する。

<?php

        switch ($errors->type) {
            case Zend_Controller_Plugin_Custom_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_Custom_ErrorHandler::EXCEPTION_NO_ACTION:
                // 404 エラー -- コントローラあるいはアクションが見つかりません
                $this->getResponse()->setRawHeader('HTTP/1.1 404 Not Found');

                $content =<<<EOH
<h1>エラー!</h1>
<p>そのページは存在しません。</p>
EOH;
                break;
            /**
             * 先ほど作成した例外がスローされた時の処理
             */
            case Zend_Controller_Plugin_Custom_ErrorHandler::EXCEPTION_ORIGINAL:

       〜処理を何なりと〜
 
                break;
            default:
                // アプリケーションのエラー
                $content =<<<EOH
<h1>エラー!</h1>
<p>予期せぬエラーが発生しました。後でもう一度お試しください。</p>
EOH;
                break;
        }

このフローを繰り返す事で、いくらでも例外ハンドリングを追加できるはず。

拡張したErrorHandlerプラグインをセットする

次にindex.php(FrontController)内で、拡張したプラグインをセットする。

<?php

// オリジナルのエラーハンドラー無効化
$front->unregisterPlugin('Zend_Controller_Plugin_ErrorHandler');
// カスタムしたエラーハンドラーを登録。
$front->registerPlugin(new Zend_Controller_Plugin_Custom_ErrorHandler);

これで動くはず。
早速、要所にtry〜catch構文を仕込み、先ほど作成した例外クラスをスローさせるようにしてみた。

<?php

throw new Zend_Original_Exception('エラーが発生したよ', 101);

ちなみに、Exceptinクラスの第一引数は文字列型のエラーメッセージ、第二引数は整数型のエラーコードを渡せる。


これで動作させるも、なぜかErrorControllerで確認できる例外タイプは「EXCEPTION_ORIGINAL」ではなく、「EXCEPTION_OTHER」だった。。。

しばらくハマるも、ソースを見て原因が分かった。
FrontControllerのregisterPlugin()メソッドで、Zend_Controller_Plugin_Brokerクラス内のregisterPlugin()メソッドをコールしているが、その中でstackIndexという名前で、登録されたプラグインに番号が振られており、そのstackIndexが大きいものの方が優先されるらしい。
ちなみにErrorHandler.phpはstackIndex = 100 で登録されていた。
このstackIndexは、registerPlugin()メソッドの第二引数で指定できるようだ。

なので、

<?php

// オリジナルのエラーハンドラー無効化
$front->unregisterPlugin('Zend_Controller_Plugin_ErrorHandler');
// カスタムしたエラーハンドラーを登録。優先して使用されるように、stackIndexを200で登録。オリジナルは100。
$front->registerPlugin(new Zend_Controller_Plugin_Custom_ErrorHandler, 200);

という風に、大幅に上回る数値を設定してやった。これで正常に動作した。
unregisterPlugin()メソッドでオリジナルのerrorHandlerは無効化したはずなのに、なぜstackIndexが影響してくるかまでは調べてません。
(ちなみにstackIndex101とかでも大丈夫だった)

追記

Front.php (FrontController)の中を見てみると、

<?php

    public function dispatch(Zend_Controller_Request_Abstract $request = null, Zend_Controller_Response_Abstract $response = null)
    {
        if (!$this->getParam('noErrorHandler') && !$this->_plugins->hasPlugin('Zend_Controller_Plugin_ErrorHandler')) {
            // Register with stack index of 100
            require_once 'Zend/Controller/Plugin/ErrorHandler.php';
            $this->_plugins->registerPlugin(new Zend_Controller_Plugin_ErrorHandler(), 100);
        }

という感じで、dispatch()メソッド内でプラグイン関係の設定を行っている事が分かった。

つまり、dispatch前に

// オリジナルのエラーハンドラー無効化
$front->unregisterPlugin('Zend_Controller_Plugin_ErrorHandler');

等としても無意味なんですね。Front.phpよく見てなかった。
やはりソース見る時は上のレイヤーからよく見ていかないと遠回りになったりするなあ。。。

Exceptionクラスをget_class_methodsした

内容は以下。

getMessage() // 例外メッセージ

getCode() // 例外をスローした時に第二引数で設定したエラーコード(10桁の整数が指定できる)

getFile() // 例外がスローされた元のスクリプトのファイルパス

getLine() // 例外がスローされたスクリプトの行数

getTrace() // 例外がスローされた時点で持っているオブジェクトの情報

getTraceAsString() // 例外がスローされた時点で持っているオブジェクトのファイル名と何を行っていたかを表示

__toString() // getTraceAsStringに、getMessage()の内容を付加したもの。エラーログ等の素材に向いていると思われる。


ところで例外処理って、「ほとんどのケースがif文で解決可能だよね」と言われたりするのですが、このZFの例のように、例外をトリガーにして様々な処理を行ったりできるのがif文には無い強みかなと思ったりしました。