异常处理篇之异常信息报告、渲染及自定义处理

上篇教程学院君大致介绍了 Laravel 异常处理底层原理,今天我们来详细介绍下进行异常处理时如何报告和渲染异常信息。

异常信息的报告

通过上篇教程的介绍,你应该知道 Laravel 是通过系统注册的异常处理器提供的 report 方法来报告异常,那么我们就来看看系统自带的异常处理器是如何定义该方法的,如果你要自定义异常报告机制的话,也可以参考该方法的实现。该方法的源码位于 vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php 中:

public function report(Exception $e)
{
    if ($this->shouldntReport($e)) {
        return;
    }

    if (is_callable($reportCallable = [$e, 'report'])) {
        return $this->container->call($reportCallable);
    }

    try {
        $logger = $this->container->make(LoggerInterface::class);
    } catch (Exception $ex) {
        throw $e;
    }

    $logger->error(
        $e->getMessage(),
        array_merge($this->context(), ['exception' => $e]
    ));
}

首先调用处理器实例的 shouldntReport 方法判断当前异常是否是不需要报告的异常,讲到这里我们需要声明下在 Laravel 应用中,开发者可以指定哪些异常不需要报告,比如一些 HTTP 404 异常,验证失败异常,我们会通过页面渲染的方式显式提示用户,没有必要再报告给系统,在异常处理器基类中,就内置了一些不需要报告的异常:

protected $internalDontReport = [
    AuthenticationException::class,
    AuthorizationException::class,
    HttpException::class,
    HttpResponseException::class,
    ModelNotFoundException::class,
    TokenMismatchException::class,
    ValidationException::class,
];

此外,我们还可以在继承了该基类的、应用级别的异常处理器类中通过 $dontReport 属性来指定额外的不需要报告的异常(如果使用系统默认的设置,对应的异常处理器类就是 App\Exceptions\Handler):

/**
 * A list of the exception types that are not reported.
 *
 * @var array
 */
protected $dontReport = [
    //
];

对于不需要报告的异常类型,Laravel 会直接跳过,不再进行报告处理,转而继续进行渲染或其它处理;否则,如果待处理异常类定义了 report 方法,将调用该异常实例上的 report 方法并返回;如果没有定义该方法,将会通过日志方式以错误级别的日志信息将该异常记录到位于 storage/logs 下的日志文件中。

以上就是 Laravel 框架底层报告异常信息的逻辑,如果你想要自定义异常信息的报告,比如跳过,可以将其配置到系统默认异常处理器的 $dontReport 属性中;而如果你想要自定义某个异常的报告逻辑,可以在该异常类中实现 report 方法,当然,你还可以选择在应用级别的异常处理器类中自定义 report 方法对异常信息的报告进行统一处理,比如将异常信息报告到 Sentry 或者 Bugsnag(相关实现参考这篇教程)。否则,系统都会将其记录到日志文件中。

异常信息的渲染

Laravel 异常渲染通过异常处理器的 render 方法完成,相应的核心逻辑也是位于 vendor/laravel/framework/src/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);
}

首先,会判断异常类中是否包含 render 方法,如果包含的话,则执行该方法,然后将结果传递给 Router::toResponse 方法处理并以响应方式返回给客户端;如果不包含该方法的话,则判断异常实例是否是 Responsable 类型,如果是的话,也会调用实例上的 toResponse 方法以响应方式返回给客户端。

如果以上两个条件不满足的话,则继续往下处理,调用 prepareException 方法为待渲染异常做一些准备工作:

protected function prepareException(Exception $e)
{
    if ($e instanceof ModelNotFoundException) {
        $e = new NotFoundHttpException($e->getMessage(), $e);
    } elseif ($e instanceof AuthorizationException) {
        $e = new AccessDeniedHttpException($e->getMessage(), $e);
    } elseif ($e instanceof TokenMismatchException) {
        $e = new HttpException(419, $e->getMessage(), $e);
    }

    return $e;
}

比如将 ModelNotFoundException 异常转化为 NotFoundHttpException 异常进行处理等,具体看代码就一目了然了,就不一一列举了。

接下来,系统会对异常类型做一些判断:

  • 如果是 HttpResponseException 异常的话,则直接调用该异常实例的 getResponse 方法并返回,比如调用 abort 辅助函数就会抛出该异常;
  • 如果是 AuthorizationException 异常的话,则调用异常处理器的 unauthenticated 方法并返回响应,如果是 API 接口请求的话,会返回 JSON 格式的 401 响应,如果是 Web 请求的话,则重定向到登录页,比如认证中间件判断用户未登录则会抛出该异常;
  • 如果是 ValidationException 异常的话,则调用异常处理器的 convertValidationExceptionToResponse 方法并返回响应,该方法主要返回验证失败信息,和上面一个判断一样,也分 JSON 接口和 Web 请求返回不同格式的响应数据,表单验证失败时通常会抛出该异常。

如果异常不是上述几种类型的异常,则最终会判断请求是否是期望 JSON 格式数据(比如 Ajax、Pjax 请求,或者请求 Content-Type 字段值是 application/json),如果是的话调用 prepareJsonResponse 方法将异常转化为 JSON 响应返回;否则调用 prepareResponse 方法返回响应:

protected function prepareResponse($request, Exception $e)
{
    if (! $this->isHttpException($e) && config('app.debug')) {
        return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
    }

    if (! $this->isHttpException($e)) {
        $e = new HttpException(500, $e->getMessage());
    }

    return $this->toIlluminateResponse(
        $this->renderHttpException($e), $e
    );
}

在该方法中,会首先判断异常是否是 HttpException,如果不是并且应用 app.debug 配置值为 true 的话,则将异常转化为 Illuminate Response 并返回;如果 app.debug 配置值为 false 或未设置的话则将异常转化为 HttpException,并将错误码设置为 500;如果异常是 HttpException 的话,则将其渲染到异常视图并返回给客户端。

以上就是 Laravel 底层处理异常渲染的逻辑,如果你需要自定义的话,可以在自定义的异常处理器中重写相关逻辑,也可以在系统自带的异常处理器 App\Exceptions\Handler 中覆盖父类相关实现来实现异常渲染的局部自定义,对于个别需要特殊处理的异常,还可以在异常类中实现 render 方法来自定义异常渲染逻辑。

上一篇: 异常处理篇之底层源码剖析

下一篇: Laravel 框架如何基于 Composer 实现类和文件的自动加载