2024-07-23

Laravelのエラーハンドリングの仕組み

Laravelのエラーハンドリングのコードリーディングメモ。

適当なコントローラーでExceptionをスローしたときのケースを追っていきます。

Exceptionがスローされると Pipeline::prepareDestination() 内のcatch句でハンドリングされます。

protected function prepareDestination(Closure $destination)
{
    return function ($passable) use ($destination) {
        try {
            return $destination($passable);
        } catch (Throwable $e) {
            return $this->handleException($passable, $e);
        }
    };
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Pipeline/Pipeline.php#L140-L149

handleException() はExceptionHandlerに紐づくクラスをインスタンス化し、report() を呼び出しつつ render() でレスポンスを返します。

protected function handleException($passable, Throwable $e)
{
    if (! $this->container->bound(ExceptionHandler::class) ||
        ! $passable instanceof Request) {
        throw $e;
    }

    $handler = $this->container->make(ExceptionHandler::class);

    $handler->report($e);

    $response = $handler->render($passable, $e);

    if (is_object($response) && method_exists($response, 'withException')) {
        $response->withException($e);
    }

    return $this->handleCarry($response);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Routing/Pipeline.php#L40-L58

report

report()shouldntReport() でレポートするかの判定を行い、レポート対象であれば reportThrowable() でレポートします。

public function report(Throwable $e)
{
    $e = $this->mapException($e);

    if ($this->shouldntReport($e)) {
        return;
    }

    $this->reportThrowable($e);
}

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

shouldntReport() ではスローされたExceptionが $dontReport $internalDontReport のインスタンスであればレポートしない処理が入っています。

protected function shouldntReport(Throwable $e)
{
    if ($this->withoutDuplicates && ($this->reportedExceptionMap[$e] ?? false)) {
        return true;
    }

    $dontReport = array_merge($this->dontReport, $this->internalDontReport);

    if (! is_null(Arr::first($dontReport, fn ($type) => $e instanceof $type))) {
        return true;
    }

    return rescue(fn () => with($this->throttle($e), function ($throttle) use ($e) {
        if ($throttle instanceof Unlimited || $throttle === null) {
            return false;
        }

        if ($throttle instanceof Lottery) {
            return ! $throttle($e);
        }

        return ! $this->container->make(RateLimiter::class)->attempt(
            with($throttle->key ?: 'illuminate:foundation:exceptions:'.$e::class, fn ($key) => $this->hashThrottleKeys ? md5($key) : $key),
            $throttle->maxAttempts,
            fn () => true,
            $throttle->decaySeconds
        );
    }), rescue: false, report: false);
}

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

レポート処理である reportThrowable() では reportable() で設定された $reportCallbacks の処理を順に実行して、最後にロギングします。

protected function reportThrowable(Throwable $e): void
{
    $this->reportedExceptionMap[$e] = true;

    if (Reflector::isCallable($reportCallable = [$e, 'report']) &&
        $this->container->call($reportCallable) !== false) {
        return;
    }

    foreach ($this->reportCallbacks as $reportCallback) {
        if ($reportCallback->handles($e) && $reportCallback($e) === false) {
            return;
        }
    }

    try {
        $logger = $this->newLogger();
    } catch (Exception) {
        throw $e;
    }

    $level = Arr::first(
        $this->levels, fn ($level, $type) => $e instanceof $type, LogLevel::ERROR
    );

    $context = $this->buildExceptionContext($e);

    method_exists($logger, $level)
        ? $logger->{$level}($e->getMessage(), $context)
        : $logger->log($level, $e->getMessage(), $context);
}

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

render

render() はExceptionに render() メソッドが生えていればそれを呼んで返します。そうでなければ各クラスに応じて処理を行います。

public function render($request, Throwable $e)
{
    $e = $this->mapException($e);

    if (method_exists($e, 'render') && $response = $e->render($request)) {
        return $this->finalizeRenderedResponse(
            $request,
            Router::toResponse($request, $response),
            $e
        );
    }

    if ($e instanceof Responsable) {
        return $this->finalizeRenderedResponse($request, $e->toResponse($request), $e);
    }

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

    if ($response = $this->renderViaCallbacks($request, $e)) {
        return $this->finalizeRenderedResponse($request, $response, $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#L559-L587

例えばValidationExceptionであれば invalid() を呼んでRedirect&Flashでバリデーションエラーを表示しています。

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

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