さいとブログ

Laravel8でJsonレスポンスの形式を統一して、エラー処理をする方法

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 エラー処理を実装

ここからはルーティング時の 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"
}


参考


プロフィール

profile icon

saitoです。
ソフトウェアエンジニアとして働いています。
web開発に関する学びを当ブログに書き残しています。