php 错误和异常的知识点比我想象的要复杂,话不多说,直接上干货。

抛开php,如果让我们去设计一个错误异常处理模块,我们需要关注哪些点?

我想应该有这几方面:

  • 错误类型有哪些 error_types
  • 出现了错误之后是否需要报告 error_report
  • 程序报告了错误之后是否需要输出错误 error_display
  • 程序报告了错误之后是否需要记录错误 error_log
  • 需要让应用程序自定义错误处理函数,set_error_handler
  • 需要让应用程序能够获取错误的信息 error_get_last

以上就是错误处理的基本知识点,这样梳理,知识点就清楚多了,搞清楚了之后上面的问题后,甚至我们自己都可以设计一个错误处理模块

异常怎么处理呢,和错误一样,我们需要考虑:

  • 异常的类型有哪些 Exception types
  • 自定义捕获异常 try catch
  • 如果没有自定义捕获异常的逻辑,那么没有捕获的异常怎么处理 set_exception_handler

PHP错误异常处理

error_types(错误类型)很重要,我们需要搞清楚错误类型有哪些,为什么会触发这些错误类型

E_ERROR 致命的运行错误,这类错误一般是不可恢复的情况,例如内存的分配问题,后果是导致脚本不在继续执行,脚本不在继续执行所以set_error_handlder 不会捕获E_ERROR, 在脚本中定义的init_set函数也不会生效

Fatal error: require(): Failed opening required 'test.php'

E_WARNING 运行的警告,比如include 一个不存在的文件,脚本会继续执行

Warning: include(test.php): failed to open stream:

E_PARSE 编译时语法解析错误 比如少了一个;

Parse error: syntax error

php内核错误

E_CORE_ERROR 在PHP初始化启动过程中发生的致命错误。该错误类似 E_ERROR,但是是由PHP引擎核心产生的

E_COMPILE_ERROR 致命编译时错误。类似E_ERROR, 但是是由Zend脚本引擎产生的。

E_COMPILE_WARNING 编译时警告 (非致命错误)。类似 E_WARNING,但是是由Zend脚本引擎产生的

E_CORE_WARNING PHP初始化启动过程中发生的警告 (非致命错误) 。类似 E_WARNING,但是是由PHP引擎核心产生的

这里需要搞清楚php引擎和zend脚本引擎到底是什么

简单来说,php引擎是启动php,初始化php的模块,读取和解析php.ini文件,激活zend引擎。

zend引擎是对脚本文件进行词法分析,语法分析。然后编译成opcode执行,如果安装了apc之类的opcode缓存,那么编译环节可能会被跳过而直接执行opcode。

opcode就是zend引擎定义的指令。

所以由php引擎,或者zend引擎触发的错误类型一般都是致命的错误,会导致脚本终止执行。所以set_error_handler无法捕获的这类错误。

php错误相关配置 php.ini

error_report 可以配置需要报告的错误类型, 一般在程序运行的时候需要报告所有的错误。


// 关闭所有PHP错误报告
error_reporting(0);

// 报告所有 PHP 错误 (参见 changelog)
error_reporting(E_ALL);

// 报告所有 PHP 错误, 和E_ALL不一样的地方在于-1可以报告未来可能出现的php错误
error_reporting(-1);

// 和 error_reporting(E_ALL); 一样
ini_set('error_reporting', E_ALL);

报告之后,就是需要判断是否将错误信息作为输出的一部分显示到屏幕,在调试模式下,我们当然希望把错误输出出来,

但是在线上一定不要输出错误信息

 # 禁止错误输出到屏幕
 ini_set('display_errors', 'off');

NOTICE 如果php脚本发生致命错误的时候,比如E_CORE_ERROR, 那么脚本会停止执行,所以在脚本中设置是无效的,只有php.ini的配置会生效

所以在线上的环境中,php.ini应该设置display_errors=off

虽然我们可能不需要把输出到屏幕,但是一定要把错误记录到日志里面,特别是线上环境,于是需要开启php错误日志

log_errors: true,
# 注意这个文件的可写权限
error_log: /tmp/php_error.log

自定义错误处理函数

php可以让用户自定义错误处理函数,重要的是要记住 error_types 里指定的错误类型都会绕过 PHP 标准错误处理程序, 除非回调函数返回了 FALSE。 error_reporting() 设置将不会起到作用而你的错误处理函数继续会被调用。

以下级别的错误不能由用户定义的函数来处理: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING

mixed set_error_handler ( callable $error_handler [, int $error_types = E_ALL | E_STRICT ] )

class HandlerErrors
{
    public function registerErrorHandler()
    {
        set_error_handler(array($this, "handleError"));
    }

    public function handleError($level, $message, $file = '', $line = 0, $context = [])
    {
        # error_reporting 获取配置报告的错误类型
        if (error_reporting() & $level) {
            throw new ErrorException($message, 0, $level, $file, $line);
        }
    }
}

error_get_last 获取最后发生的错误, 甚至可以捕获Fatal error

The error_get_last() function will give you the most recent error even when that error is a Fatal error

这个函数一般结合register_shutdown_function来使用


protected function registerShutdownHandler()
{
    register_shutdown_function(array($this, 'handleShutdown'));
}

public function handleShutdown()
{
    if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
        throw new ErrorException(
        $error['message'], $error['type'], 0, $error['file'], $error['line']
    );
    }
}

到这里,错误处理已经讲完,下面是异常处理

异常类型

所有的异常类型都继承了Exception, 在PHP7后,Exception都继承了Throwable, 不同于传统(PHP 5)的错误报告机制,现在大多数错误被作为 Error 异常抛出, 如果尚未注册异常处理函数,则按照传统方式处理:被报告为一个致命错误(Fatal Error)

Error 类并非继承自 Exception 类


# 参数的类型不要设置为Exception,因为如果是抛出了ErrorException
# 并没有继承 Exception
public function handleException(Exception $e)
{
   // TODO
}

# 正确做法
public function handleException(Throwable $e)
{
   // TODO
}

在SPL异常,PHP为我们定义了如下几种异常

BadFunctionCallException
BadMethodCallException
DomainException
InvalidArgumentException
LengthException
LogicException
OutOfBoundsException
OutOfRangeException
OverflowException
RangeException
RuntimeException
UnderflowException
UnexpectedValueException

所以,我们在写业务代码的时候,不要习惯抛出Exception, 我们可以不同的场景抛出不同的异常。

捕获异常

如果在业务代码中使用try_catch, 则异常会被捕获,如果没有设置try_catch, 我们可以定义全局函数set_exception_handler 捕获异常

常规try_catch做法

try {
   # 业务代码
} catch (Exception $e) {
  # 异常处理

} catch (ErrorException $e) {
   # php7 错误处理
} finnaly {
   # 最终都会执行到这里
}

自定义异常处理函数

class ErrorHandler
{
    protected function registerExceptionHandler()
    {
        set_exception_handler(array($this, "handleException"));
    }

    public function handleException(throwable $e)
    {
        # 获取处理throwable的handler
        $handler = $this->getExceptionHandler();

        $handler->report($e);
        if ($this->app->debugging()) {
            $this->renderForConsole($e);
        }
    }

    # laravel中需要bind的异常处理对象
    protected function getExceptionHandler()
    {
        return $this->app->make(ExceptionHandler::class);
    }

}

到这里,我们基本上了解了php是怎么处理错误和异常的,那么laravel是如何组织这些特性的呢

laravel的异常处理

laravel处理的流程是这样的:

1 配置error_report, 设置set_error_handler, set_excepiton_handler 函数

2 设置set_shutdown_function回调函数,函数执行error_get_last()获取错误

3 error_handler把错误拿到后再把错误以异常的方式throw处理统一交给excepiton_handler处理

4 在exception_handler中调用在容器中bind的exception handler去处理异常

所以在laravel 可以自己实现一个Exception handler对象

# 在handle exception 中会调用getExceptionHandler 处理异常
protected function getExceptionHandler()
{
    return $this->app->make(ExceptionHandler::class);
}

Exception Handler 对象设计

当Exception Handler拿到一个对象后,会怎么处理了,在laravel Contracts中定义了几个方法


shouldntReport: 是否需要记录这个错误
report: 记录错误到日志中
render: 输出异常,把异常输出到浏览器中
renderForConsole: 输出异常,把异常输出到屏幕

public function report(Exception $e)
{
    if ($this->shouldntReport($e)) {
        return;
    }
    try {
        $logger = app('log');
    } catch (Exception $ex) {
        throw $e;
    }

    $logger->error($e);
}

public function renderForConsole($output, Exception $e)
{
    (new ConsoleApplication)->renderException($e, $output);
}

业务异常处理

在业务开发中,我们会抛出一些业务异常,例如请求参数不合法,或者业务代码有问题(比如RPC请求超时)

这里可特别建议,严格遵守HTTP CODE 的规范来定义Exception Code

如果是请求参数不合法,抛出Exception Code 为4xx

如果是业务代码有问题,抛出Exception Code 为5xx

 # 请求餐厅的参数不合法,所以抛出400
$restaurant = Restaurant::get($id);
if (! $restaurant) {
    throw new AppException(400, 'NO_VALID_RESTAURANT');
}


# 业务代码有问题,抛出了500
try {
    $response = $this->sendHttpRequest($url, $params);
} catch (Exception $e) {
    throw new AppException(500, 'SMS_SEND_FAILED');
}

抛出异常和异常CODE的另外一个好处,就是可以使用异常的CODE重写HTTP CODE, 这样客户端可以根据HTTP CODE 的返回值做相应的逻辑判断

具体的做法是,catch住异常并获取到CODE,然后把CODE设置给Response对象,由response对象把code返回给client

# statusCode 就是http code
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);

当一次请求完成后,需要记录此次访问日志,使用CODE另外一个好处就是根据CODE 判断日志的级别

App::finish(function ($request, $response) {
    echo "excute finishn";
    $statusCode = $response->getStatusCode();

    $level = 'info';
    if ($statusCode >= 500) {
        $level = 'error';
    } elseif ($statusCode >= 400) {
        $level = 'warning';
    }

    $route = Route::current();
    $actionName = $route->getActionName();
    $actionTime = intval(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']);
    $message =
        $actionName
        .' '.json_encode($request->getParameters(), true)
        .' '.$actionTime
        .' '.$response->getStatusCode();

    Log::$level($message)
}

现在,对PHP的错误处理应该有一个系统的了解了,最重要的是学习php异常处理的思路.