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が出力されては困ります。
そこで次のようにルーティングを設定しました。
return [
'_root_' => 'system/index',
'_403_' => 'system/403',
'_404_' => 'system/404',
'_500_' => 'system/500',
];
return [];
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_を定義しても「そんなの知らない」とスルーされます。
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は直前に定義されている無名関数です。
$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クラスを
確認したところ
$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も加えます。
//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以下とします)
このファイルに変更を加えます。
変更したいのはコンストラクタだけなので他はごっそり削除で。
<?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に登録して
Autoloader::add_classes([
//Requestクラスを差し替え
'Request' => APPPATH.'classes/extend/request.php',
]);
これでOKです。