【メモ】Laravel　API　エラーフォーマットの上書きと注意点

## APIのエラーの上書き

### 概要
- 今回の要件
    - POSTやPATCHなどの引数エラーなどなど、すべてのエラーはJSON形式でレスポンスをしたい
- Laravelのデフォルトでは、HTML形式でエラーがレスポンスされてしまう。
- エラーレスポンスも含めてレスポンスの形式を制御したい。

 ### 状況説明
 ```
 Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1364 Field &#039;unique_text&#039; doesn&#039;t have a default value (Connection: mysql, SQL: insert into `card_formats` (`payment_name`, `updated_at`, `created_at`) values (rakuten, 2023-12-03 09:46:59, 2023-12-03 09:46:59)) in file /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php on line 822
```
今回のエラーは、unique_textのデフォルトの値がないにもかかわらず、DBを作成しようとした結果発生したものである。
実際に作成するAPI要件としてunique_textがnullであることは認めていない必須のパラメータである。
これに対しての方法として一般的な対処としては、事前にバリデーションチェックをしっかりと行うことである。

事前生成済みとして話は進めていくが、App\Http\Requestsのstoreファイルにある
```php:store.php
    public function rules(): array
    {
        return [
            'payment_name' => 'required|string|min:1|max:50',
            'unique_text' => 'required|string|min:1|max:255',
            'regexs' => 'required|string|min:7|max:255',
        ];
    }
```
のように、設定を行うことで、バリデーションチェックを行いエラーを回避することができる。

しかし、デフォルトの設定だと、400 Bad Requestは返らずにwelcomeページにリダイレクトしてしまう。

jsonで、エラー発生時間等も含めて返したい身としては使い勝手が悪い。

次から、jsonでエラーを返すようにする方法を取ろうと思う。

### 対策
```php:Handler.php
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * The list of the inputs that are never flashed to the session on validation exceptions.
     *
     * @var array<int, string>
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     */
    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            //
        });
    }
    public function render($request, Throwable $exception)
    {
        if ($exception instanceof ValidationException) {
            // バリデーションエラーの場合のカスタムレスポンス
            return response()->json([
                'success' => false,
                'message' => 'バリデーションエラーです',
                'errors' => $exception->errors(),
            ], 400); // ステータスコードを400に設定
        }

        if ($request->expectsJson()) {
            // その他の例外に対するJSONレスポンス
            return response()->json([
                'success' => false,
                'message' => $exception->getMessage(),
            ], $this->getStatusCode($exception));
        }

        return parent::render($request, $exception);
    }

    protected function getStatusCode($exception)
    {
        if (method_exists($exception, 'getStatusCode')) {
            return $exception->getStatusCode();
        }

        return 500; // デフォルトステータスコード
    }
}
```
Handler.phpにrender関数を追加することで、例外タイプが**ValidationException**であるときのレスポンスをラップして返すことで、指定した形式でレスポンスを返すことができるようになる。

```json:response
{
    "success": false,
    "message": "バリデーションエラーです",
    "errors": {
        "regexs": [
            "The regexs field must be at least 7 characters."
        ]
    }
}
```
このような形で、レスポンスが返ってくる。

### 問題点
しかし、この状態だとほかのエラーもラップしているはずが、うまくラップできていないため、HTMLでレスポンスが返ってきてしまう場合がある。
```php:ほかの例外に対処しているはずの部分
        if ($request->expectsJson()) {
            // その他の例外に対するJSONレスポンス
            return response()->json([
                'success' => false,
                'message' => $exception->getMessage(),
            ], $this->getStatusCode($exception));
        }
```
```html:レスポンス
<!DOCTYPE html>
<html lang="en" class="auto">
<!--
Illuminate\Database\UniqueConstraintViolationException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry &#039;rakute&#039; for key &#039;card_formats.PRIMARY&#039; (Connection: mysql, SQL: insert into `card_formats` (`payment_name`, `unique_text`, `regexs`, `updated_at`, `created_at`) values (rakute, ksjfh, kkkkkokoko, 2023-12-04 02:23:59, 2023-12-04 02:23:59)) in file /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php on line 817
```
今回は、主キーである値が重複したため発生している。
例えこのような状態であったとしても、JSONの形式でレスポンスを返したい。

ほかの例外に対処している部分で、対処されていないことからコードを読み解くと、、
```php:
    public function render($request, Throwable $exception)
    {
        if ($exception instanceof ValidationException) {
            // バリデーションエラーの場合のカスタムレスポンス
            return response()->json([
                'success' => false,
                'message' => 'バリデーションエラーです',
                'errors' => $exception->errors(),
            ], 400); // ステータスコードを400に設定
        }

        // ここのif文が問題である！！！
        if ($request->expectsJson()) {
            // その他の例外に対するJSONレスポンス
            return response()->json([
                'success' => false,
                'message' => $exception->getMessage(),
            ], $this->getStatusCode($exception));
        }
        //　さらに、ここですべてのif文に一致しなかった場合には親クラスのrenderに渡して処理を行わせている。
        return parent::render($request, $exception);
    }
```
```php:原因
$request->expectsJson()
return parent::render($request, $exception);
```
どうやら、この部分はHTTPヘッダーの**Accept**が[application/json]になっていない場合は、falseとなり、[application/json]の場合はtrueとなるようだ。

今回はPostmanのデフォルトの[*/*]の状態で行っていたため、後続の処理が行われず、HTMLの状態で帰ってきてしまった。

また、すべてのレスポンスをJSON形式にするのであれば、親のrenderに引き渡す必要がないため、削除すべきである。

### 最終的なHandler.phpの中身
```php:Handler.php
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * The list of the inputs that are never flashed to the session on validation exceptions.
     *
     * @var array<int, string>
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     */
    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            //
        });
    }
    public function render($request, Throwable $exception)
    {
        if ($exception instanceof ValidationException) {
            // バリデーションエラーの場合のカスタムレスポンス
            return response()->json([
                'success' => false,
                'message' => 'バリデーションエラーです',
                'errors' => $exception->errors(),
            ], 400); // ステータスコードを400に設定
        }
        // その他の例外に対するJSONレスポンス
        return response()->json([
            'success' => false,
            'message' => $exception->getMessage(),
        ], $this->getStatusCode($exception));
    }

    protected function getStatusCode($exception)
    {
        if (method_exists($exception, 'getStatusCode')) {
            return $exception->getStatusCode();
        }

        return 500; // デフォルトステータスコード
    }
}
```
ただこの状態だと内部のエラー内容がすべて出てしまうため、基本的には「サーバー内エラーです。」程度の情報を返す方が良いだろう。

### まとめ
意外と手間取ったが、Laravelがオブジェクト指向で作られているため、比較的修正が容易であった。最終的には、エラーに関しては量が増えやすいので、カスタムエラークラスをほかで作成して順次読み込む方が良いだろう。