过去的PHP,处理致命错误几乎是不可能的。致命错误不会调用由set_error_handler()
设置的处理方式,而是简单的停止脚本的执行。
在PHP7中,当致命错误和可捕获的错误(E_ERROR和E_RECOVERABLE_ERROR)发生时会抛出异常,而不是直接停止脚本的运行。对于某些情况,比如内存溢出,致命错误则仍然像之前一样直接停止脚本执行。在PHP7中,一个未捕获的异常也会是一个致命错误。这意味着在PHP5.x中致命错误抛出的异常未捕获,在PHP7中也是致命错误。
注意:其他级别的错误如warning和notice,在PHP7中没有变化,只有fatal和recoverable级别的错误会抛出异常。
从fatal和recoverable级别错误抛出的异常并非继承自Exception类。这种分离是为了防止现有PHP5.x的用于停止脚本运行的代码也捕获到错误抛出的异常。fatal和recoverable级别的错误抛出的异常是一个全新分离出来的类——Error类的实例。跟其他异常一样,Error类异常也能被捕获和处理,同样允许在finally之类的块结构中运行。
在先前PHP7 alpha-2时候,PHP7中异常的继承关系是不同的。fatal和recoverable级别错误抛出的异常是EngineException类的实例,不继承自Exception。Exception和EngineException都继承自BaseException。现在这个层级划分修订自我写的Throwable接口。我觉得改用Throwable和Error更简洁更吸引人,一个类用“Exception”作后缀却又不是Exception类,人们容易混淆。
Throwable
为了统一两个异常分支,Exception和Error都实现了一个全新的接口:Throwable。
PHP7中新的异常结构如下:
interface Throwable |- Exception implements Throwable |- ... |- Error implements Throwable |- TypeError extends Error |- ParseError extends Error |- ArithmeticError extends Error |- DivisionByZeroError extends ArithmeticError |- AssertionError extends Error
如果在PHP7的代码中定义了Throwable类,它将会是如下这样:
interface Throwable { public function getMessage(): string; public function getCode(): int; public function getFile(): string; public function getLine(): int; public function getTrace(): array; public function getTraceAsString(): string; public function getPrevious(): Throwable; public function __toString(): string; }
这个接口看起来很熟悉。Throwable规定的方法跟Exception几乎是一样的。唯一不同的是Throwable::getPrevious()返回的是Throwable的实例而不是Exception的。Exception和Error的构造函数跟之前Exception一样,可以接受任何Throwable的实例。
Throwable可以用于try/catch块中捕获Exception和Error对象(或是任何未来可能的异常类型)。记住捕获更多特定类型的异常并且对之做相应的处理是更好的实践。然而在某种情况下我们想捕获任何类型的异常(比如日志或框架中错误处理)。在PHP7中,要捕获所有的应该使用Throwable而不是Exception。
try { // Code that may throw an Exception or Error. } catch (Throwable $t) { // Handle exception }
用户定义的类不能实现Throwable接口。做出这个决定一定程度上是为了预测性和一致性——只有Exception和Error的对象可以被抛出。此外,异常需要携带对象在追溯堆栈中创建位置的信息,而用户定义的对象不会自动的有参数来存储这些信息。
Throwable可以被继承从而创建特定的包接口或者添加额外的方法。一个继承自Throwable的接口只能被Exception或Error的子类来实现。
interface MyPackageThrowable extends Throwable {} class MyPackageException extends Exception implements MyPackageThrowable {} throw new MyPackageException();
Error
事实上,PHP5.x中所有的错误都是fatal或recoverable级别的错误,在PHP7中都能抛出一个Error实例。跟其他任何异常一样,Error对象可以使用try/catch块来捕获。
$var = 1; try { $var->method(); // Throws an Error object in PHP 7. } catch (Error $e) { // Handle error }
通常情况下,之前的致命错误都会抛出一个基本的Error类实例,但某些错误会抛出一个更具体的Error子类:TypeError、ParseError以及AssertionError。
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
ParseError
当include/require文件或eval()’d代码存在语法错误时,ParseError会被抛出。
try { require 'file-with-parse-error.php'; } catch (ParseError $e) { echo $e->getMessage(), "\n"; }
ArithmeticError
ArithmeticError在两种情况下会被抛出。一是位移操作负数位。二是调用intdiv()时分子是PHP_INT_MIN且分母是-1(这个使用除尘运算符的表达式:PHP_INT_MIN / -1,结果是浮点型)。
try { $value = 1 << -1; catch (ArithmeticError $e) { echo $e->getMessage();//Bit shift by negative number }
DevisionByZeroError
当intdiv()的分母是0或者取模操作(%)中分母是0时,DivisionByZeroError会被抛出。注意在除法运算符(/)中使用0作除数(也即xxx/0这样写)时只会触发一个warning,这时候若分子非零结果是INF,若分子是0结果是NaN。
try { $value = 1 % 0; } catch (DivisionByZeroError $e) { echo $e->getMessage();//Modulo by zero }
AssertionError
当assert()的条件不满足时,AssertionError会被抛出。
ini_set('zend.assertions', 1); ini_set('assert.exception', 1); $test = 1; assert($test === 0); //Fatal error: Uncaught AssertionError: assert($test === 0)
只有断言启用并且是设置ini配置的zend.assertions = 1和assert.exception = 1时,assert()才会执行并抛AssertionError。
在你的代码中使用Error
用户可以通过继承Error来创建符合自己层级要求的Error类。这就形成了一个问题:什么情况下应该抛出Exception,什么情况下应该抛出Error。
Error应该用来表示需要程序员关注的代码问题。从PHP引擎抛出的Error对象属于这些分类,通常都是代码级别的错误,比如传递了错误类型的参数给一个函数或者解析一个文件发生错误。Exception则应该用于在运行时能安全的处理,并且另一个动作能继续执行的情况。
由于Error对象不应该在运行时被处理,因此捕获Error对象也应该是不频繁的。一般来说,Error对象仅被捕获用于日志记录、执行必要的清理以及展示错误信息给用户。
编写代码支持PHP5.x和PHP7的异常
为了在同样的代码中捕获任何PHP5.x和PHP7的异常,可以使用多个catch,先捕获Throwable,然后是Exception。当PHP5.x不再需要支持时,捕获Exception的catch块可以移除。
try { // Code that may throw an Exception or Error. } catch (Throwable $t) { // Executed only in PHP 7, will not match in PHP 5.x } catch (Exception $e) { // Executed only in PHP 5.x, will not be reached in PHP 7 }
不幸的是,处理异常的函数中的类型声明不容易确定。当Exception用于函数参数类型声明时,如果函数调用时候能用Error的实例,这个类型声明就要去掉。当PHP5.x不需要被支持时,类型声明则可以还原为Throwable。
翻译自:https://trowski.com/2015/06/24/throwable-exceptions-and-errors-in-php7/