はじめに
こんにちは!
Laravelでapiを作っている時に、http例外(400系、500系)が発生した時に決まった形のJsonでフロントに返したい時ありますよね。
そんな時にレスポンスの形式を作成して、エラー処理を行う方法を実装したので、忘れないように書いていきます。
今回実装する物については、管理画面とAPIを同じLaravelプロジェクトで作成しているため、API系のエラーと管理画面側のエラーを分ける必要があります。
※ Laravel歴1ヶ月ほどで書いていますので、誤記や改善点等ございましたら、ご教示いただけると幸いです。
環境
PHP 8.0.6
Laravel 8.38.0
実装
レスポンスのフォーマットを作成
まず最初にJsonレスポンスの形式を作ります。
レスポンスマクロという機能を使い、フォーマットを作成します。これをプロバイダーに実装・登録をして、どこからでも使えるようにします!
プロバイダーの作成
下記のコマンドでapp/Http/Providers
配下に元ファイルが作成されます。
php artisan make:provider ApiResponseServiceProvider
レスポンスマクロを用いて実装
ApiResponseServiceProvider.phpにレスポンスの形式を記述します。ApiResponseServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Response;
class ResponseApiServiceProvider extends ServiceProvider
{
public function boot()
{
// Error (4xx, 5xx)
Response::macro('error', function ($status, $message) {
return response()->json([
'code' => $status,
'message' => $message,
], $status);
});
}
次にapp/config/app.php
に登録します。
app.php
'providers' => [
// 省略
/*
* Application Service Providers...
*/
// 省略
App\Providers\ResponseApiServiceProvider::class,
],
これでresponse()->error(404, 'Not Found')
という形でプロジェクト内で利用できるようになります。
ちなみに以下のようなレスポンスを返します。
{
"code": 404,
"message": "Not Found"
}
エラー処理の実装
Laravelの例外は全てapp/Exceptions/Handler.php
で処理されます。
Illuminate\Foundation\Exceptions\Handler
を継承しているので、詳しい処理はここに記述されています。
Handler.phpに追加することで、デフォルトのエラー処理を拡張することができます。
Handler.php
use \Symfony\Component\HttpFoundation\Response
// 省略
public function render($request, $exception)
{
/**
* APIエラーの場合、apiErrorResponseをcall
* WEBエラーの場合、ここでエラーハンドリングを完結する
*/
if ($request->is('api/*')) {
return $this->apiErrorResponse($request, $exception);
}
return parent::render($request, $exception);
}
private function apiErrorResponse($request, $exception)
{
if ($this->isHttpException($exception)) {
$statusCode = $exception->getStatusCode();
switch ($statusCode) {
case 400:
return response()->error(Response::HTTP_BAD_REQUEST, 'Bad Request');
case 401:
return response()->error(Response::HTTP_UNAUTHORIZED, 'Unauthorized');
case 403:
return response()->error(Response::HTTP_FORBIDDEN, 'Forbidden');
case 404:
return response()->error(Response::HTTP_NOT_FOUND, 'Not Found');
}
}
}
簡単に処理の要約を書くと、webルートとapiルートで発生したエラーは区別しないといけないため、if ($request->is('api/*'))
で条件分岐しています。(apiルートのみの実装の場合、区別する必要がないです)
webの場合、継承元のrender($request, $exception)
で処理を行い、apiの場合、$this->apiErrorResponse($request, $exception)
に処理を投げます。
apiErrorResponseで行っている処理は、Httpエラーの場合、発生したエラーのステータスコードをを取得して、switch文で条件分岐しています。
条件分岐の中では、ApiResponseServiceProvider.php
で作成したレスポンスマクロを呼んでいます。
ここまで基本的なエラーを処理することができます。
捕捉ですが、Response::HTTP_BAD_REQUEST
は403の定数名です。マジックナンバーを書かないために\Symfony\Component\HttpFoundation\Response
に定義されているのを使用しています。
use Symfony\Component\HttpFoundation\Response
をファイル内でuse文で呼ぶことで使用できるようになります。
ルーティング時の403エラー処理を実装
ここからはルーティング時の403エラー処理を実装します。
以下のような感じでルーティングにauth:apiなどのmiddlewareを挟んでいる場合は、ログインしていないと利用できないため、403エラーを返して欲しいですよね。
api.php
Route::middleware('auth:api')->group(function () {
Route::post('logout', [AuthController::class, 'logout'])->name('logout');
});
Laravel標準のままだと、以下のエラーが返ります。
Symfony\Component\Routing\Exception\RouteNotFoundException: Route [login] not defined. in file /var/www/html/work/vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php on line 429
これはapp/Http/Middleware/Authenticate.php
で処理されているため、ここを拡張することで403エラーを返すことができます。
Authenticate.php
protected function redirectTo($request)
{
// 以下3行を追加
if ($request->is('api/*')) {
abort(Response::HTTP_FORBIDDEN);
};
if (! $request->expectsJson()) {
return route('login');
}
}
ここでもapiルートで発生したエラーの場合は、abort(403)
で意図的に403エラーを発生させて、Handler.phpで処理してもらいます。
これで以下の形式でレスポンスを受け取ることができます。
{
"code": 403,
"message": "Forbidden"
}
参考
LaravelでAPIを作る際のエラーハンドリングメモ
Laravelのレスポンス拡張 (正常/異常/メンテ)
入門Laravelチュートリアル (10) エラーハンドリング
Laravelでのエラー処理の流れを把握したいのでソースコードリーディング
LaravelでHTTPステータスコードの定数を使用する