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


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が出力されては困ります。
そこで次のようにルーティングを設定しました。

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

```php:modules/admin/routes.php
return [];
```

```php: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＿を定義しても「そんなの知らない」とスルーされます。


```php: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は直前に定義されている無名関数です。

```php: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してみます。

 ```php
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クラスを
確認したところ

```php: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も加えます。

```php: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以下とします）
このファイルに変更を加えます。
変更したいのはコンストラクタだけなので他はごっそり削除で。

```php: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に登録して

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

これでOKです。