小滕的博客

小滕的技术点滴

Laravel源码分析之异常处理的背后逻辑

1 year ago · 1 MIN READ
#Laravel 

今天来分析下 Laravel 背后的异常处理代码。

首先,定位到 /bootstrap/app.php 文件:

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

该调用声明了凡是需要 Illuminate\Contracts\Debug\ExceptionHandler::class 的地方都用 App\Exceptions\Handler::class 来处理,于是,进入到这个类当中:

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    protected $dontReport = [
        \App\Exceptions\ApiException::class,
        \App\Exceptions\ApiError::class,
    ];

    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    public function render($request, Exception $exception)
    {
        return parent::render($request, $exception);
    }
}

该类两个属性两个方法,很好理解,从名称就可以看出:

$dontReport 声明哪些异常不需要 report
$dontFlash 声明在发生异常的时候用户传给服务端的哪些数据不需要保存到 flash 当中,这个可以配置 old() 函数显示的
report() 通知当前异常
render() 处理当前异常

从这个类可以看出 Laravel 的异常处理考虑非常周到,不仅仅是异常的处理还报错异常的通知,在这里,我们可以结合 sentry 做异常监控。

因为 report()render() 方法都是调用了父类的方法实现的,因此异常处理的主要逻辑还是在父类当中,于是,锁定到 Illuminate\Foundation\Exceptions\Handler::class

<?php

namespace Illuminate\Foundation\Exceptions;

class Handler implements ExceptionHandlerContract
{

    // ...

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

        if (method_exists($e, 'report')) {
            return $e->report();
        }

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

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

    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);
    }

    // ...

}

由于篇幅过长,这里我删除了代码的注释。

这个类文件我们主要看两个方法:report()render() 方法。

首先 report() 方法内部首先对当前的异常进行是否 dontReport 的检测,之后判断当前异常是否定义了 report() 方法,如果定义了就执行当前异常下的 report() 方法,这里的话,我们可以在一些比较重要的异常中定义 report 方法,在发生此类异常的时候可以结合 Laravel 的消息通知进行异常通知。,最后将异常信息记录到日志当中。

其次,来分析 render() 方法:

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

从上面的代码中可以看出,它检测了 当前异常是否定义了 render() 方法,那么在这里可以延伸下 :可以针对异常进行单独的响应渲染,例如可以自定义返回的视图和 JSON 数据。如果没有实现 render() 方法,也可以这样:

class DemoException extends Exception implements Illuminate\Contracts\Support\Responsable {

    public function toResponse($request);

}

实现 Responsable 中的 toResponse() 方法,在该方法中返回一个 \Illuminate\Http\Response 实例就可以了,也可以做到自定义返回的数据。

接下来:

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

该段代码对系统内置的异常进行了一些封装,比如说将 TokenMismatchException 异常封装为 HttpException 异常等。

紧接着:

        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);
protected function renderHttpException(HttpException $e)
    {
        $status = $e->getStatusCode();

        $paths = collect(config('view.paths'));

        view()->replaceNamespace('errors', $paths->map(function ($path) {
            return "{$path}/errors";
        })->push(__DIR__.'/views')->all());

        if (view()->exists($view = "errors::{$status}")) {
            return response()->view($view, ['exception' => $e], $status, $e->getHeaders());
        }

        return $this->convertExceptionToResponse($e);
    }

这里拿到最终 HtppException 异常 render() 的方法,可以明显看出,它是正对浏览器端请求返回了视图,而视图的所在位置如下:

$paths = collect(config('view.paths'));

view()->replaceNamespace('errors', $paths->map(function ($path) {
            return "{$path}/errors";
        })->push(__DIR__.'/views')->all());

注册了视图的命名空间,这样我们就可以在 resources/views/errors 直接检错对应状态的视图文件了,如: 503.blade.php 文件,然后:

return response()->view($view, ['exception' => $e], $status, $e->getHeaders());

返回视图,响应给浏览器。

···

xiao teng



备案号:皖ICP备14012032号-5