流程启动:
#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,通过这个信息可以控制怎么渲染输出(特别是控制怎么跳转)。