背景
web画面とAPI2つを同時に提供しようしたときに、例外検知時の基本ルールとして
- web画面側:エラーページに遷移させたい(エラーページ用のhtmlを返却
- API側:error情報を格納したJSONを返却したい
とやりたいことが大きく異なりました。Handlerクラス内でif文をチマチマ書くのがしっくり来なかったのでいっその事Handlerクラスを切り替えることは出来ないかなーというのがきっかけでした。
実装方法
コンテナに登録されているExceptionHandlerをサービスプロバイダーで登録し直します。
登録し直すかどうかはURLを条件に判定します。アプリケーション固有のURL直後にapi/
が続く場合はAPI用のHandlerクラスを使用し、それ以外は通常のHandlerクラスを使用します。
※この判定ルールは各々の環境に合わせて適宜書き換えてください。
* Bootstrap any application services.
*
* @return void
*/
public function boot() {
// API関連は独自のHandlerクラスをコンテナへ再登録する
if (strpos(\Request::getPathInfo(), '/api/') === 0) {
$this->app->bind(ExceptionHandler::class, \App\Exceptions\ApiHandler::class);
}
}
サンプルとしてModelNotFoundExceptionがスローされた場合にerrorオブジェクトをJSON形式で返却させてみます。QueryBuilderのfindOrFailなどを呼び出して対象レコードが見つからなかった場合が下記分岐に当てはまります。
それ以外のfunctionはデフォルトのHandlerクラスをそのままコピーしています。
class ApiHandler extends ExceptionHandler {
/**
* Prepare response containing exception render.
*
* {@inheritdoc}
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Symfony\Component\HttpFoundation\Response
* @see \Illuminate\Foundation\Exceptions\Handler::prepareResponse()
*/
protected function prepareResponse($request, Exception $e) {
if ($e->getPrevious() instanceof ModelNotFoundException) {
return response()->json([
'error' => [
'code' => 9999
'message' => '対象のデータが見つかりませんでした。'
]
], 404);
}
}
}
残課題
今の状況だとphpunitを使用してテストを行ったときにApiHandlerが使用されないという問題が発生しています。
対応方法を確認次第追記する予定です。
(2017/05/17追記)
原因は以下のような感じでした。
1. phpunitを通してアプリケーションの初期化
2. テストクラスのインスタンス生成
3. AppServiceProviderが実行される
4. URLをもとにHandlerクラスを振り分ける
5. テストメソッド単位で順次実行
6. MakesHttpRequests::call()でHttpRequestを発生させる
→ここで初めて~/api/の様なURLが確定するので、手順3 の段階ではURLが空っぽになっているためApiHandlerをサービスコンテナに再登録する処理が実行されない。
API関連のテストクラスの親に当たるクラスを新たに作成し、setUpメソッド内でサービスコンテナにHandlerクラスを再登録させることで回避しました。
<?php
namespace Tests;
use Tests\TestCase;
class ApiTestCase extends TestCase {
public function setUp() {
parent::setUp();
$this->app->bind(\Illuminate\Contracts\Debug\ExceptionHandler::class, \App\Exceptions\ApiHandler::class);
}
}