わざわざ残す必要もないことを整理とか気にせずに吐き出していく。そんな空間。

Fuelphpのエラーハンドリングがなんか今ひとつ物足りなかったのでなんとかしてみた話

fuelphpアドベントカレンダー2013です。もう2013年ですね。早いですね。そうこうしているうちに2014年になります。なんとも恐ろしい。

思えば今年もfuelphpでした。もうこの子しか愛せなさすぎて辛い。ポリアモリーを自称する割にはこういう所は変に一途だったりするのです(あとはまぁ眼鏡とか時計とかカバンとか)。

そういえば去年は何書いたかなぁ…と思ってfuelphpアドベントカレンダー2012を見に行ったら、自分の担当のリンクだけ「お探しのページは見つかりません」。というわけで本日は404のお話です。

fuelphpのエラーハンドリングは何かと複雑です。便利機能が却って便利じゃなかったりとか、公式ドキュエントがindex.php書き換えたらイイヨ!!とか、もうなんかしったかめっちゃかな状況ですが、ざっと以下のような流れになっているみたいです。

  • リクエストの処理中にコントローラレベルでcatchされない例外が発生!!
  • HttpNotFoundException系列のみindex.phpにてキャッチ
  • [index.php] config/routeに_404_設定アレば、そちらで処理
  • [index.php] 無かったらそのまま例外横流し
  • 今度はエラーハンドラ(Error::exception_handler)がキャッチ
  • [エラーハンドラ] 例外オブジェクトがhandleメソド持ってたらそちらで処理
  • [エラーハンドラ] handleが無くて本番環境なら,errors/productionをレンダする。
  • [エラーハンドラ] handleが無くて本番でも無ければ、errors/php_fatal_errorをレンダする。

errors/productionとかerrors/php_fatal_errorはcoreに入ってる方のView。同名ファイル作ればAPP側で上書きできる。productionってのはあの「Oops!」ってやつで、php_fatal_errorってのはあの開発中に便利なバックトレースとかつけてくれる例外画面。

んで、ドキュメントを見るとなにやらHttpNotFoundExceptionって例外を投げるとエラー画面を描画してくれる、とかある。これはCoreに入ってるHttpNotFoundExceptionって例外クラスに例のhandleメソドが実装されているからそうなるわけで、描画されるViewはviews/404.phpになる。

やりたいこと

CIのエラーハンドリングでもそうなんだけど、実際アプリケーションの開発では1システム1エラー画面というわけには行かない。

PC版のリクエストならPC版の404を出したいし、SP版のリクエストならSP版の404を出したい。もっと言うならAPIのリクエストではJSON形式の404メッセージを送りたい(はず)。

CI使ってた時にはエラーのView内で条件分岐して、リクエストの種別(PC/SP)毎にそれぞれ表示するView書き換えたりしてたので、それを応用しながら上手いこと出来ないかなぁとか考えてたら以下のような形になりました。

class HttpNotFoundException extends \Fuel\Core\HttpNotFoundException
{
    public function response()
    {
        Fuel::$profiling = false;
        //デフォルト404出力の定義
        //$response = Response::forge(View::forge('404'),404);//デフォルト
        $response = Request::forge("top/404")->execute()->response();//デフォルト

        $req = Request::forge();
        $req->action = "404";
        try{
            $response = $req->execute()->response();
        }catch(Exception $e){
            //何もしない
        }
        $response->set_status(404);
        return $response;
    }
}

各リクエストのURL形式からコントローラの検出まで可能であればそのコントローラにaction_404が存在しないかをチェックし、あればそれを出力する、なければデフォルトの404出力を返すというものです。

index.phpはいじらずにすみますが、coreのHttpNotFoundExceptionを上書きするのでbootstrapに以下のような記述が必要になります。

Autoloader::add_classes(array(
    // Add classes you want to override here
    // Example: 'View' => APPPATH.'classes/view.php',
    'HttpNotFoundException' => APPPATH.'classes/httpnotfoundexception.php'// 上記クラスを記述した場所
));

クラス名が_の全くない長い名前なので律儀にfuelphpの命名規則に従う必要はないです。上記bootstrapの記述でしっかりパス指定さえ行えばどこに置いてもオートロードします。

解説

        Fuel::$profiling = false;

これを入れておかないとプロファイラが先に出力されて、HTMLがおかしくなる。端的に言うと文字が化ける。

        //デフォルト404出力の定義
        //$response = Response::forge(View::forge('404'),404);//デフォルト
        $response = Request::forge("top/404")->execute()->response();//デフォルト

コメントアウトしてある上の行はViewファイルを直接指定するタイプのデフォルト404指定。コアのHttpNotFoundExceptionの挙動に近い形。その下の行はUriで直接指定するタイプ。config/router.php_404_の書き方に近い形。
それぞれお好きな方をご利用ください。

        $req = Request::forge();
        $req->action = "404";
        try{
            $response = $req->execute()->response();
        }catch(Exception $e){
            //何もしない
        }

例外の処理内部でリクエストを再生成して、actionだけ書き換えてからリトライする形。 try catch しとかないと多重ループする気がする。もともとaction_404に対してのダメ元リトライなので catchしてもとくにすることはない。

あとがき

 Controllerごとにエラーハンドリングしたいなーって思いは結構前々からあったのですが、 そこまでしっかりした開発をfuelphpでやる機会もしばらく無く、ようやくこの機会に着手する事が出来ました。

正直views/404.phpが着地点なんだから、そこからRequest::active()なりRequest::main()なりで controllerのインスタンス引っ張ってきてaction_404のコールをトライしたら終わりじゃね?くらいの軽い気持ちだったのですが、 よくよく処理を追いかけなおして見ると、バッチリreset_requestなるメソドが張り巡らされており、404.phpはおろか、 HttpNotFoundExceptionからもControllerのインスタンスは取得できませんでした。 まさかコントローラを再生性するはめにはなるとは…

 fuelphp的なコントローラは基本的に生成コスト低め、のはずなので問題無いとは思いますが ファットなコントローラで生成コスト高め(特にbeforeがごちゃごちゃしてる…)の時には、処理負担的にあまりオススメできませんが、 参考になれば幸いです。