PHP 中 Error 和 Exception 两种异常的特性及日志记录或显示

PHP 文档:
Error
Exception

参考:
深入理解PHP原理之异常机制
我们什么时候应该使用异常
异常和错误

所有示例基于 PHP7。

应用中,关于错误的最佳实践是:

  • 必须报告错误
  • 开发环境要显示错误,生产环境不可显示
  • 开发环境和生产环境都要记录错误日志

Error 和 Exception 的异同

  • Exception 需要通过 throw new Exception 手动抛出
  • Error 可以在 PHP 脚本执行发生错误时自动触发,也可以通过 trigger_errors() 手动触发
  • 都实现了 Throwable 接口,可以通过 catch (Throwable $t) {...} 同时捕获 Error 和 Exception
  • 如果不捕获并处理 Exception,程序会终止,并报出 Fatal Error 错误,但捕获后程序可以继续执行
  • catch (Error $e) { ... },或者通过注册错误处理函数( set_error_handler())来捕获 Error
  • catch (Exception $e) { ... } 或者通过注册异常处理函数( set_exception_handler())来捕获 Exception
  • catch (Throwable $e) { ... } 可以同时捕获 Exception 和 Error


echo 1/0;
echo 666;
echo 1%0;
echo 666;
PHP Warning: Division by zero in /code/main.php on line 3
INF666
PHP Fatal error:  Uncaught DivisionByZeroError: Modulo by zero in /code/main.php:5

Throwable

用户定义的类无法实现 Throwable,所以用户只能抛出 Exception 或 Error 的实例。扩展 Throwable 的接口只能通过扩展 Exception 或 Error 的类来实现。

继承关系

Error 类 和 Exception 类 都继承自 Throwable 接口,不同版本的继承关系可以参考 这里。下面是 7.2.0 - 7.2.7 的继承关系:

Error
   ArithmeticError
      DivisionByZeroError
   AssertionError
   ParseError
   TypeError
      ArgumentCountError
Exception
   ClosedGeneratorException
   DOMException
   ErrorException
   IntlException
   LogicException
      BadFunctionCallException
         BadMethodCallException
      DomainException
      InvalidArgumentException
      LengthException
      OutOfRangeException
   PharException
   ReflectionException
   RuntimeException
      OutOfBoundsException
      OverflowException
      PDOException
      RangeException
      UnderflowException
      UnexpectedValueException
   SodiumException

常用方法

完整的接口可以参考 这里。

  • getMessage — 获取异常消息内容
  • getCode — 获取异常代码
  • getFile — 导致异常的程序文件名称
  • getLine — 导致异常的行号

Error

从 PHP 7 开始,大多数错误(致命错误和可恢复错误)被作为 Error 异常抛出,从而可以捕获并处理,防止脚本终止执行。与任何其他 Exception 异常一样,可以使用 try / catch 块捕获 Error 对象。

从致命(fatal)和可恢复(recoverable)的错误中抛出的异常并没有继承 Exception,而是继承自 Error。

Error 的严重等级

Parse error > Fatal Error > Waning > Notice > Deprecated

错误名称 解释 可能的原因 程序是否中止 如何捕获错误 备注
Parse error 语法错误 代码解析失败 中断执行 PHP7 之后可以用 catch (Error $e) { ... } 捕获
Fatal Error 运行时错误 实例化不存在的类,调不存在的方法 中断执行 PHP7 之后可以用 catch (Error $e) { ... } 捕获 可以使用 register_shutdown_function() 函数设置一个在 PHP 中止前执行收尾工作的函数
Waning 警告 四则运算时出现非数字 继续执行 可以用 set_error_handler() 捕获
Notice 注意 变量或数组下标未定义 继续执行 可以用 set_error_handler() 捕获
Deprecated 使用了废弃函数 函数已经废弃 继续执行 可以用 set_error_handler() 捕获

Error 处理流程

  1. 先看看有没有匹配的 catch 块(注意是 Error 类型而不是 Exception 类型:catch (Error $e) { ... }),如果有则被第一个匹配的 try / catch 块所捕获。
  2. 如果没有没有匹配的 catch 块,则去调用异常处理函数(事先通过 set_error_handler() 注册)进行处理(仅用于 Deprecated、Notice、Waning 这三种级别)。
  3. 如果尚未注册异常处理函数,则按照传统方式处理:报告错误(Fatal Error 等)。

PHP 生成的每个错误都包含一个类型。类型列表以及它们的行为及其产生方式的简短描述可以参考 这里。常用的有:

常量 说明
1 E_ERROR 致命的运行时错误。不可捕捉,不可恢复。脚本终止运行。
2 E_WARNING 运行时警告 (非致命错误)。仅给出提示信息,但是脚本不会终止运行。
256 E_USER_ERROR 用户产生的错误信息。类似 E_ERROR, 但是是由用户自己在代码中使用函数 trigger_error() 触发的。
512 E_USER_WARNING 用户产生的警告信息。类似 E_WARNING, 但是是由用户自己在代码中使用PHP函数 trigger_error() 触发的。
2048 E_STRICT (integer) 启用 PHP 对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性。
4096 E_RECOVERABLE_ERROR 可被捕捉的致命错误。它表示发生了一个可能非常危险的错误,但是还没有导致 PHP 引擎处于不稳定的状态。如果该错误没有被用户自定义处理程序捕获(set_error_handler()),将成为一个 E_ERROR 从而脚本会终止运行。
8192 E_DEPRECATED 运行时通知。对在未来版本中可能无法正常工作的代码给出警告。
30719 E_ALL E_STRICT 外的所有错误和警告信息。

设置 PHP 配置文件来处理错误

设置报告错误的等级

如果未设置错误处理程序,则 PHP 将根据 php.ini 配置文件处理发生的任何错误。error_reporting 指令控制报告和忽略哪些错误。虽然也可以在运行时通过调用 error_reporting() 函数来控制,但强烈建议设置配置指令,因为在脚本开始执行之前也可能会发生一些错误。

在开发环境中,为了了解并解决 PHP 引发的问题,最好将 error_reporting 设置为 E_ALL 来记录所有的错误。生产环境中,可以将 error_reporting 设置为 E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED 来避免记录过多信息,但是在多数情况因为下 E_ALL 可以提供早期预警,记录潜在的问题,也可以用于生产环境。

显示错误或记录日志

发生错误时,PHP 可以采取两种措施,由另外两个 php.ini 指令设置:

  • display_errors:输出错误。生产环境中必须禁用,因为它可能包含机密信息(如数据库密码),但可用于开发环境,确保立即报告问题。
  • log_errors:记录错误日志。这会将任何错误记录到 error_log 定义的文件或 syslog 中。这在生产环境中非常有用,可以记录发生的错误,然后根据这些错误生成报告。

用户自定义错误处理程序

如果 PHP 的默认错误处理不满足需求,还可以使用 set_error_handler() 安装自己的自定义错误处理程序来处理许多类型的错误。

一般用于处理用户通过 trigger_error 触发的错误,大部分 PHP 内置错误类型无法以这种方式处理。可以按照脚本认为合适的方式处理那些可以处理的错误类型:例如,向用户显示自定义错误页面,然后直接发送电子邮件报告错误,而不是通过日志。

set_error_handler('myErrorHandler');

function myErrorHandler($severity, $message, $filepath, $line) {
    echo "错误信息:".$message;
    // 发送电子邮件...
    exit(1); // 必要时手动终止脚本
}

function myDiv($a, $b) {
    return $a/$b;
}

myDiv(1, 0);
eval('ech 66'); // 无法用自定义的错误处理程序

将 Error 变为 ErrorException:

set_error_handler('myErrorHandler');
set_exception_handler('myExceptionHandler');

function myExceptionHandler($exception) {
    echo $exception->getMessage();
}
function myErrorHandler($severity, $message, $file, $line)
{
    if (!(error_reporting() & $severity)) {
        // This error code is not included in error_reporting, so let it fall
        // through to the standard PHP error handler
        return false;
    }
    throw new ErrorException($message, 0, $severity, $file, $line);
}

function myDiv($a, $b) {
    return $a/$b;
}

myDiv(1, 0);

具体的 Error 类

ArithmeticError 算术错误

两种可能的原因:

  • 使用负数移位
  • 调用 intdiv() 方法时,分子是 PHP_INT_MIN 且分母为 -1(此时将返回浮点数)。
try {
    $value = 1 << -1;
    intdiv(PHP_INT_MIN, -1);
} catch (ArithmeticError $e) {
    echo $e->getMessage(), "\n";
}

DivisionByZeroError

两种可能的原因:

  • 模数(%)运算时,分母为 0。
  • 调用 intdiv() 方法时,分母为 0。

注意,在除法(/)运算符中使用零做分母仅发出警告。

try {
    echo 1/0; // 仅警告
    intdiv(1, 0);
    echo 1%0;
} catch (DivisionByZeroError $e) {
    echo $e->getMessage();
}

AssertionError

使用 assert() 语言结构进行断言时,可能抛出这个错误:

ini_set('zend.assertions', 1); // 执行代码
ini_set('assert.exception', 1); // 允许抛异常

$test = 1;
assert($test === 0);

ParseError

  • 通过 included 或 required 引入文件有语法错误
  • eval() 解析的字符串有语法错误
try {
    eval('ech 66');
    include 'has-error.php';
} catch (ParseError $e) {
    echo $e->getMessage(), "\n";
}
syntax error, unexpected '66' (T_LNUMBER)

TypeError

函数的参数或返回值跟类型不匹配时,抛 TypeError:

function add(int $left, int $right)
{
    return $left + $right;
}

try {
    $value = add('left', 'right');
} catch (TypeError $e) {
    echo $e->getMessage(), "\n";
}
Argument 1 passed to add() must be of the type integer, string given, called in D:\workspace\szhz\application\controllers\tuan\Index.php on line 312

Exception

Exception 出现的原因

PHP 在使用异常机制之前,通过返回错误码来表示函数的执行结果。部分函数返回 TRUE 或 FALSE,部分函数返回 0 或 1、-1。难以统一且无法包含足够的报错原因等信息。例如 strtotime() 函数,成功则返回时间戳,否则返回 FALSE,但是在 PHP 5.1.0 之前本函数在失败时返回 -1。

异常机制避免了错误码机制的一些不足,可以在 一次捕获多个异常。异常对象包含错误信息、错误码、错误行号、文件、上下文,更方便定位问题。

Exception 特点

Exception 是必须手动抛出并且可被捕获的。如果抛出的异常未被捕获,则导致 Fatal error,并使得代码停止执行。

function myDiv($a, $b) {
    if ($b == 0) 
        throw new Exception('Divided by zero');

    return $a/$b;
}

try {
    myDiv(1, 0); // 如果不捕获异常,则报错 Fatal error,并停止执行
} catch (Exception $e) {
    echo $e->getMessage(), "\n";
}

// 异常捕获后,可以继续执行后面的代码
...

自定义 Exception

自定义的 Exception 需要继承自已有异常,定义完成后就可以在代码中抛出自定义的这些异常。



class pdoDbException extends PDOException { 
    public function __construct(PDOException $e) { 
        if(strstr($e->getMessage(), 'SQLSTATE[')) {
            echo 'this is my exception';
        } 
    } 
}

function f() {
    try { 
        $pdo = new PDO('123.207.7.188', '$username', '$password', []); 
    } catch (PDOException $e) { 
        throw new pdoDbException($e); 
    }
}

try {
    f();
} catch (pdoDbException $e) {
    print_r($e);
}

用户自定义异常处理程序

set_exception_handler('myExceptionHandler');

function myExceptionHandler($exception) {
    echo $exception->getMessage();
}

function myDiv($a, $b) {
    if ($b == 0)
        throw new Exception('Divided by zero');

    return $a/$b;
}

myDiv(1, 0);

// 自定义异常处理程序执行后,不会继续执行后面的代码

你可能感兴趣的:(PHP)