3
3

More than 5 years have passed since last update.

FuelPHP1.7.3 module内のルーティングで_403_, _500_を定義しても機能しないバグの対処法

Last updated at Posted at 2016-01-04

FuelPHP1.7.3のお話です。バージョン1.7.3ではそれまであった404のルーティングに加え
_403_、_500_が追加されました。それぞれHttpNoAccessException、HttpServerErrorExceptionを
投げると自動的に呼び出されます。ですが、これらをモジュール内で定義した場合
適切に処理がされないようなのです。

きっかけ

app
└classes
 └controller
└modules
 └admin
 └api

上記のような構成でサイトを構築していました。わりとよくあるパターンです。
通常のコントローラー(ユーザーサイド)とAdminモジュール内のコントローラーは
すべてController_Templateを継承し、レスポンスとしてHTMLを返します。
apiはController_Restを継承し、json・xmlなどを返します。
apiモジュールで404を投げたときにHTMLが出力されては困ります。
そこで次のようにルーティングを設定しました。

routes.php
return [
    '_root_'  => 'system/index',
    '_403_'   => 'system/403',
    '_404_'   => 'system/404',
    '_500_'   => 'system/500',
];
modules/admin/routes.php
return [];
modules/api/routes.php
return [
    '_403_'   => 'api/system/403',
    '_404_'   => 'api/system/404',
    '_500_'   => 'api/system/500',
];

ユーザーサイドとadminモジュールでは共通のエラー画面を表示するようにし、
jsonを返したいapiモジュールのみエラー用に別のアクションを設定しました。
これでOKと思っていたら何かおかしい。apiでエラーが起こると
ユーザーサイドのエラーレスポンス(HTML)が返ってくるのです。
さらによくよく確認してみると404はちゃんとjson形式でレスポンスを返していて
403、500の時におかしくなるようです。

原因究明

とりあえずindex.phpをのぞいてみます。
ちなみに1.7.3では_403_、_500_が追加されことに伴ってindex.phpの内容も変更されています。
index.phpはcomposerの管理対象外なので、1.7.3にバージョンアップした場合は手動で更新してあげないといけません。
index.phpが古いままだとルーティングで_403_、_500_を定義しても「そんなの知らない」とスルーされます。

index.php
try
{
    // Boot the app...
    require APPPATH.'bootstrap.php';

    // ... and execute the main request
    $response = $routerequest();
}
catch (HttpNoAccessException $e)
{
    $response = $routerequest('_403_', $e);
}
catch (HttpNotFoundException $e)
{
    $response = $routerequest('_404_', $e);
}
catch (HttpServerErrorException $e)
{
    $response = $routerequest('_500_', $e);
}

このあたりがレスポンスを生成している箇所みたいですね。
通常のルーティングでもモジュールのルーティングでも一様に_403_で検索しているところが
ヒントになりそうな予感。
$routerequestは直前に定義されている無名関数です。

index.php
$routerequest = function($route = null, $e = false)
{
    Request::reset_request(true);

    $route = array_key_exists($route, Router::$routes) ? Router::$routes[$route]->translation : Config::get('routes.'.$route);
    if ($route instanceof Closure)
    {
        $response = $route();

        if( ! $response instanceof Response)
        {
            $response = Response::forge($response);
        }
    }
    elseif ($e === false)
    {
        $response = Request::forge()->execute()->response();
    }
    elseif ($route)
    {
        $response = Request::forge($route, false)->execute(array($e))->response();
    }
    else
    {
        throw $e;
    }

    return $response;
};

Router::\$routesにルーティング内容がセットされている?
Routerクラスを見て確認したところ合っているようなので、
とりあえずRouter::\$routesをまるまるvar_dumpしてみます。

array(6) {
  ["api/_403_"]=>
  object(Fuel\Core\Route)#15 (14) {
    ["segments"]=>
    array(0) {
    }
    ["named_params"]=>
    array(0) {
    }
    ["method_params"]=>
    array(0) {
    }
    ["path"]=>
    string(9) "api/_403_"
    ["case_sensitive"]=>
    bool(true)
    ["strip_extension"]=>
    bool(true)
    ["name"]=>
    string(9) "api/_403_"
    ["module"]=>
    NULL
    ["directory"]=>
    NULL
    ["controller"]=>
    NULL
    ["action"]=>
    string(5) "index"
    ["translation"]=>
    string(14) "api/system/403"
    ["callable"]=>
    NULL
    ["search":protected]=>
    string(9) "api/_403_"
  }
  ["_404_"]=>
  object(Fuel\Core\Route)#21 (14) {
    ["segments"]=>
    array(0) {
    }
    ["named_params"]=>
    array(0) {
    }
    ["method_params"]=>
    array(0) {
    }
    ["path"]=>
    string(5) "_404_"
    ["case_sensitive"]=>
    bool(true)
    ["strip_extension"]=>
    bool(true)
    ["name"]=>
    string(5) "_404_"
    ["module"]=>
    NULL
    ["directory"]=>
    NULL
    ["controller"]=>
    NULL
    ["action"]=>
    string(5) "index"
    ["translation"]=>
    string(14) "api/system/404"
    ["callable"]=>
    NULL
    ["search":protected]=>
    string(5) "_404_"
  }
  ["api/_500_"]=>
  object(Fuel\Core\Route)#20 (14) {
    ["segments"]=>
    array(0) {
    }
    ["named_params"]=>
    array(0) {
    }
    ["method_params"]=>
    array(0) {
    }
    ["path"]=>
    string(9) "api/_500_"
    ["case_sensitive"]=>
    bool(true)
    ["strip_extension"]=>
    bool(true)
    ["name"]=>
    string(9) "api/_500_"
    ["module"]=>
    NULL
    ["directory"]=>
    NULL
    ["controller"]=>
    NULL
    ["action"]=>
    string(5) "index"
    ["translation"]=>
    string(14) "api/system/500"
    ["callable"]=>
    NULL
    ["search":protected]=>
    string(9) "api/_500_"
  }
  ["_root_"]=>
  object(Fuel\Core\Route)#12 (14) {
    ["segments"]=>
    array(0) {
    }
    ["named_params"]=>
    array(0) {
    }
    ["method_params"]=>
    array(0) {
    }
    ["path"]=>
    string(6) "_root_"
    ["case_sensitive"]=>
    bool(true)
    ["strip_extension"]=>
    bool(true)
    ["name"]=>
    string(6) "_root_"
    ["module"]=>
    NULL
    ["directory"]=>
    NULL
    ["controller"]=>
    NULL
    ["action"]=>
    string(5) "index"
    ["translation"]=>
    string(12) "system/index"
    ["callable"]=>
    NULL
    ["search":protected]=>
    string(0) ""
  }
  ["_403_"]=>
  object(Fuel\Core\Route)#14 (14) {
    ["segments"]=>
    array(0) {
    }
    ["named_params"]=>
    array(0) {
    }
    ["method_params"]=>
    array(0) {
    }
    ["path"]=>
    string(5) "_403_"
    ["case_sensitive"]=>
    bool(true)
    ["strip_extension"]=>
    bool(true)
    ["name"]=>
    string(5) "_403_"
    ["module"]=>
    NULL
    ["directory"]=>
    NULL
    ["controller"]=>
    NULL
    ["action"]=>
    string(5) "index"
    ["translation"]=>
    string(10) "system/403"
    ["callable"]=>
    NULL
    ["search":protected]=>
    string(5) "_403_"
  }
  ["_500_"]=>
  object(Fuel\Core\Route)#16 (14) {
    ["segments"]=>
    array(0) {
    }
    ["named_params"]=>
    array(0) {
    }
    ["method_params"]=>
    array(0) {
    }
    ["path"]=>
    string(5) "_500_"
    ["case_sensitive"]=>
    bool(true)
    ["strip_extension"]=>
    bool(true)
    ["name"]=>
    string(5) "_500_"
    ["module"]=>
    NULL
    ["directory"]=>
    NULL
    ["controller"]=>
    NULL
    ["action"]=>
    string(5) "index"
    ["translation"]=>
    string(10) "system/500"
    ["callable"]=>
    NULL
    ["search":protected]=>
    string(5) "_500_"
  }
}

あやしい。translationの値から察するに_404_はapiモージュールの_404_で上書きされているようなのに、
403、500は上書きされず、api/_403_、api/_500_という別のキーが追加で登録されています。
index.phpの処理内容で考えると_404_と同様にキーを上書きしないといけないはず。
これはつまり、モジュールのルーティング内容をセットする箇所で、_403_、_500_が
考慮されていない?

モジュールのルーティングをセットしている処理を探します。
その箇所ではきっと_404_をちやほやと特別扱いしているはずなので、
_404_を頼りに検索していきます。コア内のRouter、Routeなどと探していくとも
それらしき箇所は見当たらず、では処理の始点ということでRequestクラスを
確認したところ

core/classes/request.php
$prepped_routes = array();
foreach($module_routes as $name => $_route)
{
    if ($name === '_root_')
    {
        $name = $module;
    }
    elseif (strpos($name, $module.'/') !== 0 and $name != $module and $name !== '_404_')
    {
        $name = $module.'/'.$name;
    }

    $prepped_routes[$name] = $_route;
};

これだ!
Requestクラスのコンストラクタにありました。
ルーティング名が_404_でなければモジュール名を付加するという処理が行われています。
つまり_404_ならキー名はそのままで上書きです。
モジュールで404だけが正常に機能するという点とも合致します。
確認のため、ちょっとお行儀が悪いですがRequestクラスを書き換えてみましょう。
ちやほや対象に403と500も加えます。

core/classes/request.php
//old
elseif (strpos($name, $module.'/') !== 0 and $name != $module and $name !== '_404_')
//new
elseif (strpos($name, $module.'/') !== 0 and $name != $module
                            and ! in_array($name, ["_403_", "_404_", "_500_"]))

そして確認。期待通りの結果が返ってくるようになりました!

結論

  • Requestクラスのコンストラクタ内でルーティングが取りまとめられる
  • モジュールにアクセスが来て、モジュール内のルーティングに_root_、_404_など特殊な経路が定義されていた場合、大本の特殊経路を上書きすることが期待されている
  • が、新しく追加された_403_、_500_は特別扱いされていない。上書きしない
  • ので、大本の_403_、_500_が呼び出される

ということのようです。

解決策

コア上書きして(完)はまずいのでちゃんとします。
core/classes/request.phpの継承・修正版を作ってbootstrapで差し替える形にします。

先ほどcore/classes/request.phpにくわえた修正を元に戻します。
同ファイルをapp/calsess以下好きな場所にコピー(ここではapp/classes/extend以下とします)
このファイルに変更を加えます。
変更したいのはコンストラクタだけなので他はごっそり削除で。

app/classes/extend/request.php
<?php

class Request extends Fuel\Core\Request
{
    public function __construct($uri, $route = true, $method = null)
    {
        //長いので略

        //この箇所を
        elseif (strpos($name, $module.'/') !== 0 and $name != $module and $name !== '_404_')
        //こう修正
        elseif (strpos($name, $module.'/') !== 0 and $name != $module
                and ! in_array($name, ["_403_", "_404_", "_500_"]))
    }
}

bootstrap.phpに登録して

app/bootstrap.php
Autoloader::add_classes([
    //Requestクラスを差し替え
    'Request' => APPPATH.'classes/extend/request.php',
]);

これでOKです。

3
3
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3