laravel HttpResponseExceptionの引数にリソースクラス由来のHTTPレスポンスを渡す

# 概要

- laravelをAPIサーバーとして開発していると、リクエストクラスのバリデーションエラー時にJSONでエラー内容を返却する必要にかられる。
- リクエストクラスにfailedValidationメソッドを定義して、HttpResponseExceptionのインスタンスを返しても良いが、返却する内容はBadRequest系ならBadRequest系で統一されているリソースクラスを使いたい。
- その場合の方法を簡単にまとめる。

# 方法

## BadRequestリソースの作成

- バリデーションエラーのときに返すBadRequestクラス（リソース）を下記のように定義する。

    ```BadRequest.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Http\Resources\Common;
    
    use Illuminate\Http\JsonResponse;
    use Illuminate\Http\Request;
    use Illuminate\Http\Resources\Json\JsonResource;
    use Symfony\Component\HttpFoundation\Response;
    use App\Http\Resources\Base\Resource;
    
    class BadRequest extends JsonResource implements Resource
    {
        /**
         * @var string
         */
        private string $message;
    
        /**
         * 配列の中に複数の型が入ることを想定してmixedを使用
         * @var array<string|integer, mixed>|null
         */
        private ?array $info;
    
        /**
         * @param string $message
         * @param array<string|integer, mixed>|null $info
         */
        public function __construct(string $message = 'bad request', ?array $info = null)
        {
            $this->message = $message;
            $this->info = $info;
        }
    
        /**
         * Transform the resource into an array.
         *
         * @return array<string, mixed>
         */
        public function toArray(Request $request): array
        {
            return [
                'message' => $this->message,
                'info' => $this->info,
            ];
        }
    
        /**
         * レスポンスのステータスコードを指定
         *
         * @param Request $request
         * @param JsonResponse $response
         * @return void
         */
        public function withResponse(Request $request, JsonResponse $response): void
        {
            $response->setStatusCode(Response::HTTP_BAD_REQUEST);
        }
    }
    ```

## リクエストクラスのfailedValidationメソッド

- failedValidationメソッドを下記のように記載する。
- ※ちなみに筆者はリクエストクラスの親となるBaseRequestクラスを定義して、各リクエストクラスで継承することで、各リクエストクラスでfailedValidationメソッドを定義する手間を省いている。

    ```BaseRequest.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Http\Requests\Api;
    
    use App\Http\Resources\Common\BadRequest;
    use Illuminate\Foundation\Http\FormRequest;
    use Illuminate\Http\Exceptions\HttpResponseException;
    use Illuminate\Contracts\Validation\Validator;
    use Symfony\Component\HttpFoundation\Response;
    
    class BaseRequest extends FormRequest
    {
        /**
         * Determine if the user is authorized to make this request.
         */
        public function authorize(): bool
        {
            return true;
        }
    
        /**
         * ヴァリデーション失敗時の処理
         *
         * @param Validator $validator
         * @throws HttpResponseException
         * @return void
         */
        protected function failedValidation(Validator $validator): void
        {
            $errors = $validator->errors()->toArray();
            $badRequest = new BadRequest(info: $errors);
        
            throw new HttpResponseException($badRequest->response());
        }
    }

    ```

# 超ざっくりとした解説（リソースクラスからHTTPレスポンスが取得できるわけ）

- リクエストクラスのfailedValidationメソッドで`$badRequest->response();`とすることでリソースからHTTPリクエストが得られる理由を簡単に説明する。
- BadRequestクラスは`Illuminate\Http\JsonResource`クラスを継承している。
- JsonResourceクラスのresponseメソッドを下記に記載する。

    ```vendor/laravel/framework/src/Illuminate/Http/Resources/Json/JsonResource.php
    /**
     * Transform the resource into an HTTP response.
     *
     * @param  \Illuminate\Http\Request|null  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function response($request = null)
    {
        return $this->toResponse(
            $request ?: Container::getInstance()->make('request')
        );
    }

    /**
     * Create an HTTP response that represents the object.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function toResponse($request)
    {
        return (new ResourceResponse($this))->toResponse($request);
    }
    ```

- 上記のresponseメソッドはJsonReponseを返している。
- JsonResponseは継承元の継承元が`Symfony\Component\HttpFoundation\Response`であるためHttpResponseExceptionクラスのインスタンス化時の引数が`Symfony\Component\HttpFoundation\Response`のインスタンスを期待している部分で問題が発生しない。
- ※JsonResourceやJsonResponseなどぱっと見間違えそうなクラスがたくさん出てきているので注意する