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);
}
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);
}
レポート処理である 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);
}
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);
}
例えば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));
}