2024-06-18

LaravelのValidationExceptionのハンドリングの仕組み

Laravelの ValidationException のハンドリングが何をやっているのかコードリーディングメモ。


Illuminate\Foundation\Exceptions\Handler のこの部分。

public function render($request, Throwable $e)
{
    // ...(省略)...

    return $this->finalizeRenderedResponse($request, match (true) {
        $e instanceof HttpResponseException => $e->getResponse(),
        $e instanceof AuthenticationException => $this->unauthenticated($request, $e),
        $e instanceof ValidationException => $this->convertValidationExceptionToResponse($e, $request),
        default => $this->renderExceptionResponse($request, $e),
    }, $e);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/Exceptions/Handler.php#L584

convertValidationExceptionToResponse() ではJSONを返さない場合は invalid() が呼ばれます。

protected function convertValidationExceptionToResponse(ValidationException $e, $request)
{
    if ($e->response) {
        return $e->response;
    }

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

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/Exceptions/Handler.php#L720-L729

invalid() はRedirectResponseを作成しています。

protected function invalid($request, ValidationException $exception)
{
    return redirect($exception->redirectTo ?? url()->previous())
                ->withInput(Arr::except($request->input(), $this->dontFlash))
                ->withErrors($exception->errors(), $request->input('_error_bag', $exception->errorBag));
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/Exceptions/Handler.php#L738-L743

withInput()Illuminate\Session\Store::flashInput() を呼び出し、 _old_input にflashデータとして入力値をセットします。これによって入力値がリダイレクトGET後の一回限りのセッションデータとして利用できるようになります。

public function withInput(array $input = null)
{
    $this->session->flashInput($this->removeFilesFromInput(
        ! is_null($input) ? $input : $this->request->input()
    ));

    return $this;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Http/RedirectResponse.php#L74-L81 https://github.com/laravel/framework/blob/11.x/src/Illuminate/Session/Store.php#L534-L537

withErrors()Illuminate\Session\Store::flash() を叩いて ValidationException::errors() の内容をゴニョゴニョしてセットしてます。

public function withErrors($provider, $key = 'default')
{
    $value = $this->parseErrors($provider);

    $errors = $this->session->get('errors', new ViewErrorBag);

    if (! $errors instanceof ViewErrorBag) {
        $errors = new ViewErrorBag;
    }

    $this->session->flash(
        'errors', $errors->put($key, $value)
    );

    return $this;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Http/RedirectResponse.php#L131-L146

Bladeで使える old() 関数はhelperファイルに定義されています

function old($key = null, $default = null)
{
    return app('request')->old($key, $default);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Foundation/helpers.php#L570-L573

InteractsWithFlashData::old() では Illuminate\Session\Store::getOldInput() が呼ばれます。

public function old($key = null, $default = null)
{
    $default = $default instanceof Model ? $default->getAttribute($key) : $default;

    return $this->hasSession() ? $this->session()->getOldInput($key, $default) : $default;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Http/Concerns/InteractsWithFlashData.php#L16-L21

getOldInput() ではflashのセッションデータである _old_input を取得しています。これにより直前の入力値を取得できます。

public function getOldInput($key = null, $default = null)
{
    return Arr::get($this->get('_old_input', []), $key, $default);
}

エラーの値に関してはBladeの $errors で取得できますが、これは Illuminate\View\Middleware\ShareErrorsFromSession によってviewの変数にshareされることで実現しています。

public function handle($request, Closure $next)
{
    // If the current session has an "errors" variable bound to it, we will share
    // its value with all view instances so the views can easily access errors
    // without having to bind. An empty bag is set when there aren't errors.
    $this->view->share(
        'errors', $request->session()->get('errors') ?: new ViewErrorBag
    );

    // Putting the errors in the view for every view allows the developer to just
    // assume that some errors are always available, which is convenient since
    // they don't have to continually run checks for the presence of errors.

    return $next($request);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/View/Middleware/ShareErrorsFromSession.php#L36-L50

このエントリーをはてなブックマークに追加