标签归档:异常

Laravel 异常详解

流程启动:

#public/index.php

$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

首先,在__DIR__.’/../bootstrap/app.php’中,向容器注册了错误处理类:

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

然后,$kernel->handle()中,会调用kernel的bootstrap()方法,最终会运行\Illuminate\Foundation\Bootstrap\HandleExceptions::class类的bootstrap()方法:

class HandleExceptions
{
    public function bootstrap(Application $app)
    {
        $this->app = $app;

        error_reporting(-1);

        set_error_handler([$this, 'handleError']);

        set_exception_handler([$this, 'handleException']);

        register_shutdown_function([$this, 'handleShutdown']);

        if (! $app->environment('testing')) {
            ini_set('display_errors', 'Off');
        }
    }
    public function handleError($level, $message, $file = '', $line = 0, $context = [])
    {
        if (error_reporting() & $level) {
            throw new ErrorException($message, 0, $level, $file, $line);
        }
    }
    public function handleException($e)
    {
        if (! $e instanceof Exception) {
            $e = new FatalThrowableError($e);
        }

        try {
            $this->getExceptionHandler()->report($e);
        } catch (Exception $e) {
            //
        }

        if ($this->app->runningInConsole()) {
            $this->renderForConsole($e);
        } else {
            $this->renderHttpResponse($e);
        }
    }
}

这里挟持了PHP的错误和异常处理。分别对应handleError和handleException。一旦发生错误或异常,相应进入设置的回调。注意到handleError方法,如果$level是需要报告的,那么就抛出一个ErrorException异常,这里是把错误转换成了异常。

继续handleException()方法,首先如果接收的对象不是一个异常对象(Exception子类),就用FatalThrowableError类包装一下(变成异常对象)。然后调用异常处理类的report方法,接着判断是否运行在Console下,对应运行render方法。这里的$this->getExceptionHandler()方法从容器中取回绑定的异常处理对象,它就是前面绑定的App\Exceptions\Handler类对象,所以这个类应该实现了report和render方法。

最终,异常(包括错误),最终定位到App\Exceptions\Handler中的report和render方法(这两个方法都会运行):

<?php
// App\Exceptions\Handler

namespace App\Exceptions;

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

class Handler extends ExceptionHandler
{
    protected $dontReport = [
        //
    ];

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

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

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

App\Exceptions\Handler中的report和render都是直接调用父类方法(这里可以注入自定义逻辑)。

App\Exceptions\Handler继承自Illuminate\Foundation\Exceptions\Handler,Illuminate\Foundation\Exceptions\Handler实现了Illuminate\Contracts\Debug\ExceptionHandler接口。

// Illuminate\Contracts\Debug\ExceptionHandler 接口
namespace Illuminate\Contracts\Debug;

use Exception;

interface ExceptionHandler
{
    public function report(Exception $e);

    public function render($request, Exception $e);

    public function renderForConsole($output, Exception $e);
}

注,report方法就是把异常记录到外部,这个过程是由日志处理器来完成的,在Laravel中是通过Monolog来处理的(可以设置多个处理器,以让日志发送到多个地方)。

以下就是Illuminate\Foundation\Exceptions\Handler中的report实现:

//Illuminate\Foundation\Exceptions\Handler
    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]
        ));
    }

首先$this->shouldntReport()实际就是判断异常是否在$this->dontReport和$this->internalDontReport数组中,在就是不需要报告。

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

而$this->dontReport是空的,可以在子类App\Exceptions\Handler的dontReport中添加不需要报告的异常类型。

接下来,如果一个异常包含了report方法,那么直接调用它的report方法。然后从容器中取回日志处理器(框架初始化时已经注册),然后调用日志处理器的error记录日志。 这里需要注意,report方法还是可能继续抛出异常。

对于render方法就比较复杂一点,因为需要渲染输出:

//Illuminate\Foundation\Exceptions\Handler
    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);
    }

看看AuthenticationException异常的处理(认证异常):

    protected function unauthenticated($request, AuthenticationException $exception)
    {
        return $request->expectsJson()
                    ? response()->json(['message' => $exception->getMessage()], 401)
                    : redirect()->guest(route('login'));
    }

认证不通过,可能返回route(‘login’)路由。这里有点硬编码了。如果有两个后台,那么这里只要认证不通过就永远跳转到route(‘login’)。所以需要根据guard来判断跳转到哪个路由:

//App\Exceptions\Handler
    public function render($request, Exception $exception)
    {
        if ($exception instanceof \Illuminate\Auth\AuthenticationException) {
            $guards = $exception->guards();
            if (is_array($guards) && in_array('admin', $guards)) {
                return $request->expectsJson()
                    ? response()->json(['message' => $exception->getMessage()], 401)
                    : redirect()->guest(route('backend.login'));
            }
        }
        return parent::render($request, $exception);
    }

当使用默认auth中间件时,如果抛异常,$exception->guards()是空的(使用了默认的guard);当指定了guard后(比如auth:admin),这时$exception->guards()就包含admin,通过这个信息可以控制怎么渲染输出(特别是控制怎么跳转)。