Laravel で api を実装している時に、http 例外(400 系、500 系)が発生した時に決まった形の Json でクライアントに返したい時ありますよね。
そんな時にレスポンスの形式を作成して、エラー処理を行う方法を実装したので、忘れないように書いていきます。
今回実装する物については、管理画面と API を同じ Laravel プロジェクトで作成しているため、API 系のエラーと管理画面側のエラーを分ける必要があります。
環境
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 as ResponseStatus;
// 省略
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(ResponseStatus::HTTP_BAD_REQUEST, 'Bad Request');
case 401:
return response()->error(ResponseStatus::HTTP_UNAUTHORIZED, 'Unauthorized');
case 403:
return response()->error(ResponseStatus::HTTP_FORBIDDEN, 'Forbidden');
case 404:
return response()->error(ResponseStatus::HTTP_NOT_FOUND, 'Not Found');
}
}
}
簡単に処理の要約を書くと、web ルートと api ルートで発生したエラーは区別しないといけないため、 if ($request->is('api/*'))
で条件分岐しています。
web の場合、継承元のrender($request, $exception)
で処理を行い、api の場合、$this->apiErrorResponse($request, $exception)
に処理を投げます。
apiErrorResponse で行っている処理は、Http エラーの場合、発生したエラーのステータスコードをを取得して、switch 文で条件分岐しています。
条件分岐の中では、ApiResponseServiceProvider.php
で作成したレスポンスマクロを呼んでいます。
ここまで基本的なエラーを処理することができます。
ここからはルーティング時の 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(403);
};
if (! $request->expectsJson()) {
return route('login');
}
}
ここでも api ルートで発生したエラーの場合は、abort(403)
で意図的に 403 エラーを発生させて、Handler.php で処理してもらいます。
これで以下の形式でレスポンスを受け取ることができます。
{
"code": 403,
"message": "Forbidden"
}