前言

对于一个优秀的框架来说,正确的异常处理可以防止暴露自身接口给用户,可以提供快速追溯问题的提示给开发人员。本文会详细的介绍 laravel 异常处理的源码。

PHP 异常处理

本章节参考 PHP错误异常处理详解

异常处理(又称为错误处理)功能提供了处理程序运行时出现的错误或异常情况的方法。
  
异常处理通常是防止未知错误产生所采取的处理措施。异常处理的好处是你不用再绞尽脑汁去考虑各种错误,这为处理某一类错误提供了一个很有效的方法,使编程效率大大提高。当异常被触发时,通常会发生:

  • 当前代码状态被保存
  • 代码执行被切换到预定义的异常处理器函数
  • 根据情况,处理器也许会从保存的代码状态重新开始执行代码,终止脚本执行,或从代码中另外的位置继续执行脚本

PHP 5 提供了一种新的面向对象的错误处理方法。可以使用检测(try)、抛出(throw)和捕获(catch)异常。即使用try检测有没有抛出(throw)异常,若有异常抛出(throw),使用catch捕获异常。

一个 try 至少要有一个与之对应的 catch。定义多个 catch 可以捕获不同的对象。php 会按这些 catch 被定义的顺序执行,直到完成最后一个为止。而在这些 catch 内,又可以抛出新的异常。

异常的抛出

当一个异常被抛出时,其后的代码将不会继续执行,PHP 会尝试查找匹配的 catch 代码块。如果一个异常没有被捕获,而且又没用使用set_exception_handler() 作相应的处理的话,那么 PHP 将会产生一个严重的错误,并且输出未能捕获异常 (Uncaught Exception ... ) 的提示信息。

抛出异常,但不去捕获它:

  1. ini_set('display_errors', 'On');
  2. error_reporting(E_ALL & ~ E_WARNING);
  3. $error = 'Always throw this error';
  4. throw new Exception($error);
  5. // 继续执行
  6. echo 'Hello World';

上面的代码会获得类似这样的一个致命错误:

  1. Fatal error: Uncaught exception 'Exception' with message 'Always throw this error' in E:\sngrep\index.php on line 5
  2. Exception: Always throw this error in E:\sngrep\index.php on line 5
  3. Call Stack:
  4. 0.0005 330680 1. {main}() E:\sngrep\index.php:0

Try, throw 和 catch

要避免上面这个致命错误,可以使用try catch捕获掉。

处理处理程序应当包括:

  • Try - 使用异常的函数应该位于 “try” 代码块内。如果没有触发异常,则代码将照常继续执行。但是如果异常被触发,会抛出一个异常。
  • Throw - 这里规定如何触发异常。每一个 “throw” 必须对应至少一个 “catch”
  • Catch - “catch” 代码块会捕获异常,并创建一个包含异常信息的对象

抛出异常并捕获掉,可以继续执行后面的代码:

  1. try {
  2. $error = 'Always throw this error';
  3. throw new Exception($error);
  4. // 从这里开始,tra 代码块内的代码将不会被执行
  5. echo 'Never executed';
  6. } catch (Exception $e) {
  7. echo 'Caught exception: ', $e->getMessage(),'<br>';
  8. }
  9. // 继续执行
  10. echo 'Hello World';

顶层异常处理器 set_exception_handler

在我们实际开发中,异常捕捉仅仅靠 try {} catch () 是远远不够的。set_exception_handler() 函数可设置处理所有未捕获异常的用户定义函数。

  1. function myException($exception)
  2. {
  3. echo "<b>Exception:</b> " , $exception->getMessage();
  4. }
  5. set_exception_handler('myException');
  6. throw new Exception('Uncaught Exception occurred');

扩展 PHP 内置的异常处理类

用户可以用自定义的异常处理类来扩展 PHP 内置的异常处理类。以下的代码说明了在内置的异常处理类中,哪些属性和方法在子类中是可访问和可继承的。

  1. class Exception
  2. {
  3. protected $message = 'Unknown exception'; // 异常信息
  4. protected $code = 0; // 用户自定义异常代码
  5. protected $file; // 发生异常的文件名
  6. protected $line; // 发生异常的代码行号
  7. function __construct($message = null, $code = 0);
  8. final function getMessage(); // 返回异常信息
  9. final function getCode(); // 返回异常代码
  10. final function getFile(); // 返回发生异常的文件名
  11. final function getLine(); // 返回发生异常的代码行号
  12. final function getTrace(); // backtrace() 数组
  13. final function getTraceAsString(); // 已格成化成字符串的 getTrace() 信息
  14. /* 可重载的方法 */
  15. function __toString(); // 可输出的字符串
  16. }

如果使用自定义的类来扩展内置异常处理类,并且要重新定义构造函数的话,建议同时调用 parent::__construct() 来检查所有的变量是否已被赋值。当对象要输出字符串的时候,可以重载 __toString() 并自定义输出的样式。

  1. class MyException extends Exception
  2. {
  3. // 重定义构造器使 message 变为必须被指定的属性
  4. public function __construct($message, $code = 0) {
  5. // 自定义的代码
  6. // 确保所有变量都被正确赋值
  7. parent::__construct($message, $code);
  8. }
  9. // 自定义字符串输出的样式 */
  10. public function __toString() {
  11. return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
  12. }
  13. public function customFunction() {
  14. echo "A Custom function for this type of exception\n";
  15. }
  16. }

MyException 类是作为旧的 exception 类的一个扩展来创建的。这样它就继承了旧类的所有属性和方法,我们可以使用 exception 类的方法,比如 getLine()getFile() 以及 getMessage()

PHP 错误处理

PHP 的错误级别

常量 说明
1 E_ERROR 致命的运行时错误。这类错误一般是不可恢复的情况,例如内存分配导致的问题。后果是导致脚本终止不再继续运行。
2 E_WARNING 运行时警告 (非致命错误)。仅给出提示信息,但是脚本不会终止运行。
4 E_PARSE 编译时语法解析错误。解析错误仅仅由分析器产生。
8 E_NOTICE 运行时通知。表示脚本遇到可能会表现为错误的情况,但是在可以正常运行的脚本里面也可能会有类似的通知。
16 E_CORE_ERROR 在PHP初始化启动过程中发生的致命错误。该错误类似 E_ERROR,但是是由PHP引擎核心产生的。
32 E_CORE_WARNING PHP初始化启动过程中发生的警告 (非致命错误) 。类似 E_WARNING,但是是由PHP引擎核心产生的。
64 E_COMPILE_ERROR 致命编译时错误。类似E_ERROR, 但是是由Zend脚本引擎产生的。
128 E_COMPILE_WARNING 编译时警告 (非致命错误)。类似 E_WARNING,但是是由Zend脚本引擎产生的。
256 E_USER_ERROR 用户产生的错误信息。类似 E_ERROR, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。
512 E_USER_WARNING 用户产生的警告信息。类似 E_WARNING, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。
1024 E_USER_NOTICE 用户产生的通知信息。类似 E_NOTICE, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。
2048 E_STRICT 启用 PHP 对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性。
4096 E_RECOVERABLE_ERROR 可被捕捉的致命错误。 它表示发生了一个可能非常危险的错误,但是还没有导致PHP引擎处于不稳定的状态。 如果该错误没有被用户自定义句柄捕获 (参见 set_error_handler()),将成为一个 E_ERROR 从而脚本会终止运行。
8192 E_DEPRECATED 运行时通知。启用后将会对在未来版本中可能无法正常工作的代码给出警告。
16384 E_USER_DEPRECATED 用户产少的警告信息。 类似 E_DEPRECATED, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。
30719 E_ALL 用户产少的警告信息。 类似 E_DEPRECATED, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。

错误的抛出

除了系统在运行 php 代码抛出的意外错误。我们还可以利用 rigger_error 产生一个自定义的用户级别的 error/warning/notice 错误信息:

  1. if ($divisor == 0) {
  2. trigger_error("Cannot divide by zero", E_USER_ERROR);
  3. }

顶级错误处理器

顶级错误处理器 set_error_handler 一般用于捕捉 E_NOTICEE_USER_ERRORE_USER_WARNINGE_USER_NOTICE 级别的错误,不能捕捉 E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERRORE_COMPILE_WARNING

register_shutdown_function

register_shutdown_function() 函数可实现当程序执行完成后执行的函数,其功能为可实现程序执行完成的后续操作。程序在运行的时候可能存在执行超时,或强制关闭等情况,但这种情况下默认的提示是非常不友好的,如果使用 register_shutdown_function() 函数捕获异常,就能提供更加友好的错误展示方式,同时可以实现一些功能的后续操作,如执行完成后的临时数据清理,包括临时文件等。

可以这样理解调用条件:

  • 当页面被用户强制停止时
  • 当程序代码运行超时时
  • 当PHP代码执行完成时,代码执行存在异常和错误、警告

我们前面说过,set_error_handler 能够捕捉的错误类型有限,很多致命错误例如解析错误等都无法捕捉,但是这类致命错误发生时,PHP 会调用 register_shutdown_function 所注册的函数,如果结合函数 error_get_last,就会获取错误发生的信息。

Laravel 异常处理

laravel 的异常处理由类 \Illuminate\Foundation\Bootstrap\HandleExceptions::class 完成:

  1. class HandleExceptions
  2. {
  3. public function bootstrap(Application $app)
  4. {
  5. $this->app = $app;
  6. error_reporting(-1);
  7. set_error_handler([$this, 'handleError']);
  8. set_exception_handler([$this, 'handleException']);
  9. register_shutdown_function([$this, 'handleShutdown']);
  10. if (! $app->environment('testing')) {
  11. ini_set('display_errors', 'Off');
  12. }
  13. }
  14. }

异常转化

laravel 的异常处理均由函数 handleException 负责。

PHP7 实现了一个全局的 throwable 接口,原来的 Exception 和部分 Error 都实现了这个接口, 以接口的方式定义了异常的继承结构。于是,PHP7 中更多的 Error 变为可捕获的 Exception 返回给开发者,如果不进行捕获则为 Error ,如果捕获就变为一个可在程序内处理的 Exception。这些可被捕获的 Error 通常都是不会对程序造成致命伤害的 Error,例如函数不存在。

PHP7 中,基于 /Error exception,派生了5个新的engine exception:ArithmeticError / AssertionError / DivisionByZeroError / ParseError / TypeError。在 PHP7 里,无论是老的 /Exception 还是新的 /Error ,它们都实现了一个共同的interface: /Throwable

因此,遇到非 Exception 类型的异常,首先就要将其转化为 FatalThrowableError 类型:

  1. public function handleException($e)
  2. {
  3. if (! $e instanceof Exception) {
  4. $e = new FatalThrowableError($e);
  5. }
  6. $this->getExceptionHandler()->report($e);
  7. if ($this->app->runningInConsole()) {
  8. $this->renderForConsole($e);
  9. } else {
  10. $this->renderHttpResponse($e);
  11. }
  12. }

FatalThrowableErrorSymfony 继承 \ErrorException 的错误异常类:

  1. class FatalThrowableError extends FatalErrorException
  2. {
  3. public function __construct(\Throwable $e)
  4. {
  5. if ($e instanceof \ParseError) {
  6. $message = 'Parse error: '.$e->getMessage();
  7. $severity = E_PARSE;
  8. } elseif ($e instanceof \TypeError) {
  9. $message = 'Type error: '.$e->getMessage();
  10. $severity = E_RECOVERABLE_ERROR;
  11. } else {
  12. $message = $e->getMessage();
  13. $severity = E_ERROR;
  14. }
  15. \ErrorException::__construct(
  16. $message,
  17. $e->getCode(),
  18. $severity,
  19. $e->getFile(),
  20. $e->getLine()
  21. );
  22. $this->setTrace($e->getTrace());
  23. }
  24. }

异常 Log

当遇到异常情况的时候,laravel 首要做的事情就是记录 log,这个就是 report 函数的作用。

  1. protected function getExceptionHandler()
  2. {
  3. return $this->app->make(ExceptionHandler::class);
  4. }

laravelIoc 容器中默认的异常处理类是 Illuminate\Foundation\Exceptions\Handler:

  1. class Handler implements ExceptionHandlerContract
  2. {
  3. public function report(Exception $e)
  4. {
  5. if ($this->shouldntReport($e)) {
  6. return;
  7. }
  8. try {
  9. $logger = $this->container->make(LoggerInterface::class);
  10. } catch (Exception $ex) {
  11. throw $e; // throw the original exception
  12. }
  13. $logger->error($e);
  14. }
  15. protected function shouldntReport(Exception $e)
  16. {
  17. $dontReport = array_merge($this->dontReport, [HttpResponseException::class]);
  18. return ! is_null(collect($dontReport)->first(function ($type) use ($e) {
  19. return $e instanceof $type;
  20. }));
  21. }
  22. }

异常页面展示

记录 log 后,就要将异常转化为页面向开发者展示异常的信息,以便查看问题的来源:

  1. protected function renderHttpResponse(Exception $e)
  2. {
  3. $this->getExceptionHandler()->render($this->app['request'], $e)->send();
  4. }
  5. class Handler implements ExceptionHandlerContract
  6. {
  7. public function render($request, Exception $e)
  8. {
  9. $e = $this->prepareException($e);
  10. if ($e instanceof HttpResponseException) {
  11. return $e->getResponse();
  12. } elseif ($e instanceof AuthenticationException) {
  13. return $this->unauthenticated($request, $e);
  14. } elseif ($e instanceof ValidationException) {
  15. return $this->convertValidationExceptionToResponse($e, $request);
  16. }
  17. return $this->prepareResponse($request, $e);
  18. }
  19. }

对于不同的异常,laravel 有不同的处理,大致有 HttpExceptionHttpResponseExceptionAuthorizationExceptionModelNotFoundExceptionAuthenticationExceptionValidationException。由于特定的不同异常带有自身的不同需求,本文不会特别介绍。本文继续介绍最普通的异常 HttpException 的处理:

  1. protected function prepareResponse($request, Exception $e)
  2. {
  3. if ($this->isHttpException($e)) {
  4. return $this->toIlluminateResponse($this->renderHttpException($e), $e);
  5. } else {
  6. return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
  7. }
  8. }
  9. protected function renderHttpException(HttpException $e)
  10. {
  11. $status = $e->getStatusCode();
  12. view()->replaceNamespace('errors', [
  13. resource_path('views/errors'),
  14. __DIR__.'/views',
  15. ]);
  16. if (view()->exists("errors::{$status}")) {
  17. return response()->view("errors::{$status}", ['exception' => $e], $status, $e->getHeaders());
  18. } else {
  19. return $this->convertExceptionToResponse($e);
  20. }
  21. }

对于 HttpException 来说,会根据其错误的状态码,选取不同的错误页面模板,若不存在相关的模板,则会通过 SymfonyResponse 来构造异常展示页面:

  1. protected function convertExceptionToResponse(Exception $e)
  2. {
  3. $e = FlattenException::create($e);
  4. $handler = new SymfonyExceptionHandler(config('app.debug'));
  5. return SymfonyResponse::create($handler->getHtml($e), $e->getStatusCode(), $e->getHeaders());
  6. }
  7. protected function toIlluminateResponse($response, Exception $e)
  8. {
  9. if ($response instanceof SymfonyRedirectResponse) {
  10. $response = new RedirectResponse($response->getTargetUrl(), $response->getStatusCode(), $response->headers->all());
  11. } else {
  12. $response = new Response($response->getContent(), $response->getStatusCode(), $response->headers->all());
  13. }
  14. return $response->withException($e);
  15. }

laravel 错误处理

  1. public function handleError($level, $message, $file = '', $line = 0, $context = [])
  2. {
  3. if (error_reporting() & $level) {
  4. throw new ErrorException($message, 0, $level, $file, $line);
  5. }
  6. }
  7. public function handleShutdown()
  8. {
  9. if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
  10. $this->handleException($this->fatalExceptionFromError($error, 0));
  11. }
  12. }
  13. protected function fatalExceptionFromError(array $error, $traceOffset = null)
  14. {
  15. return new FatalErrorException(
  16. $error['message'], $error['type'], 0, $error['file'], $error['line'], $traceOffset
  17. );
  18. }
  19. protected function isFatal($type)
  20. {
  21. return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]);
  22. }

对于不致命的错误,例如 notice级别的错误,handleError 即可截取, laravel 将错误转化为了异常,交给了 handleException 去处理。

对于致命错误,例如 E_PARSE 解析错误,handleShutdown 将会启动,并且判断当前脚本结束是否是由于致命错误,如果是致命错误,将会将其转化为 FatalErrorException, 交给了 handleException 作为异常去处理。