WEB

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

WEB
この記事は約8分で読めます。

はじめに

こんにちは!

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ステータスコードの定数を使用する