All Articles

FormRequestのバリデーション失敗時にjsonを返す

以下どちらかの実装でjsonレスポンスにできる。

  • failedValidation()をオーバーライドする
  • ヘッダーにAcceptまたはX-Requested-Withを追加する

failedValidation()のオーバーライド

フォームリクエストクラスのfailedValidation()をオーバーライドすることでjsonレスポンスかつメッセージをカスタマイズすることも可能。以下はサインアップのエンドポイントを想定したリクエストの例。

<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class SignUpRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'user_id' => 'required',
            'password' => 'required'
        ];
    }

    public function messages()
    {
        return [
            'user_id.required' => 'ユーザーIDは必須です。',
            'password.required' => 'パスワードは必須です。',
        ];
    }

    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(
            response()->json([
                'message' => $validator->errors()->toArray(),
            ], 403)
        );
    }
}

ヘッダにAcceptまたはX-Requested-Withを追加する

エクセプションハンドラを変更していない場合、バリデーションエラー発生時はIlluminate/Foundation/Exceptions/Handler::render()でレスポンス生成が行われる。

    // Illuminate/Foundation/Exceptions/Handler.php
    public function render($request, Exception $e)
    {
        if (method_exists($e, 'render') && $response = $e->render($request)) {
            return Router::toResponse($request, $response);
        } elseif ($e instanceof Responsable) {
            return $e->toResponse($request);
        }

        $e = $this->prepareException($e);

        if ($e instanceof HttpResponseException) {
            return $e->getResponse();
        } elseif ($e instanceof AuthenticationException) {
            return $this->unauthenticated($request, $e);
        } elseif ($e instanceof ValidationException) {
            return $this->convertValidationExceptionToResponse($e, $request);
        }

        return $request->expectsJson()
                    ? $this->prepareJsonResponse($request, $e)
                    : $this->prepareResponse($request, $e);
    }

今回はバリデーションエラーの場合なのでconvertValidationExceptionToResponse()が実行される。

    // Illuminate/Foundation/Exceptions/Handler.php
    protected function convertValidationExceptionToResponse(ValidationException $e, $request)
    {
        if ($e->response) {
            return $e->response;
        }

        return $request->expectsJson()
                    ? $this->invalidJson($request, $e)
                    : $this->invalid($request, $e);
    }

expectsJson()はヘッダにX-Requested-With: XMLHttpRequestが含まれているか、ヘッダにAccept: .../jsonもしくはAccept: ...+jsonが含まれている場合にjsonレスポンスを返却する。

そのためミドルウェアでヘッダを追加することでjsonレスポンスが取得できる。

<?php
// app/Http/Middleware/EnforceJson.php

namespace App\Http\Middleware;

use Closure;

/**
 * @see https://stackoverflow.com/questions/44453221/how-to-set-header-for-all-requests-in-route-group
 */
class EnforceJson
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');
        return $next($request);
    }
}
<?php
// app/Http/Kernel.php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    // ...

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\EnforceJson::class, // 追加
        ],
    ];

    // ...
}

個人的には返却するメッセージを変更できるのでfailedValidation()をオーバーライドするほうが好みです。

参考