7. 异常与错误
当类中的某个方法遇到不可恢复的错误(文件不存在、未连接数据库、网络连接中断)时,它不会在控制台(文件、数据库)上显示错误,而是抛出一个异常。所有异常都继承自 [\Exception] 类。 除异常外,PHP 的内部运行还会抛出以 [\Error] 为基类的错误。这两个类都实现了 PHP 接口。
7.1. 脚本树

7.2. 接口 [\Throwable]
接口 [\Throwable] 如下:

该接口中各方法的作用如下:

7.3. PHP 7 中预定义的异常
PHP 7 定义了多个异常类:

- 在 [1] 中,包含 PHP 中预定义的异常;
- 在 [2] 中,包含 PHP 7 中的 SPL 库(标准 PHP 库)的异常。 SPL 库是一组旨在解决开发人员常见问题的类和接口集合。
7.4. PHP 7 中预定义的错误
PHP 7 定义了多个错误类:

[\Error] 类是 PHP 中所有预定义错误的父类。 类 [ErrorException] 允许将类 [\Error] 的实例封装在类 [\Exception] 的实例中。这使得仅处理异常,从而实现了错误管理的统一。
7.5. 示例 1
第一个示例 [exceptions-01.php] 同时展示了 PHP 错误和一个异常:
<?php
// 显示所有错误
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// 代码 --------
$var=[];
// 未知密钥
print $var["abcd"];
// 除以零
$var=7/0;
var_dump($var);
// 数组下限超出
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// 索引超出范围
$array[5]=8;
注释
- 第 4 行:要求 PHP 报告所有错误。第二个参数是所需的错误级别:


- 第5行:要求将错误显示在控制台上;
- 第 9 行:访问了数组 [$var] 中不存在的元素;
- 第 11 行:进行了除以零的操作;
- 第14行:创建类[SplFixedArray]的实例。该类用于创建具有固定边界和整数索引的数组;
- 第 18 行:访问数组中不存在的元素;
结果
注释
- 结果第1行:访问数组中不存在的键会导致级别为[E_NOTICE]的错误PHP。这不会中断脚本的执行;
- 结果第 3 行:将一个数字除以零会引发级别为 [E_WARNING] 的 PHP 错误。这不会中断脚本的执行;
- 结果第 6-9 行:访问数组中不存在的索引 [SplFixedArray] 会引发类型为 [RuntimeException] 的异常,并中断脚本的执行;
7.6. 异常处理
脚本 [exceptions-02.php] 演示了如何处理异常:
<?php
// 显示所有错误
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// 用 try / catch 包围代码
try {
$var = [];
// 未知键
print $var["abcd"];
// 除以零
$var = 7 / 0;
var_dump($var);
// 数组下界超出
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// 索引超出范围
$array[5] = 8;
// 验证
print "ce message ne sera pas affiché\n";
} catch (\Throwable $ex) {
// \Throwable 是大多数错误和异常所实现的接口
// 显示异常
print "erreur, message : " . $ex->getMessage() . ", type : " . get_class($ex) . "\n";
}
注释
- 该脚本即前一段落中介绍的脚本。只是现在,我们用 try / catch 结构将可能引发错误的第 8-19 行代码包围起来:如果第 8-21 行代码引发(抛出)异常或错误,则该异常或错误将由第 22-26 行的 catch 子句进行处理;
- 第22行:[catch]子句的参数即为需要处理的异常或错误类型。 通过将类型设置为 [\Throwable](这是一个接口),表明我们希望处理所有实现 [\Throwable] 接口的类实例。 由于所有错误和异常类都实现了该接口,因此[catch]子句在此处处理所有封装在类中的错误/异常;
- 第 19 行:触发错误并引发异常的语句。一旦发生异常,程序将跳转至 [catch] 子句。因此,第 19 行之后的代码将不会被执行;
结果
结果说明
- 第 1 行和第 3 行:发现了级别为 [E_NOTICE] 和 [E_WARNING] 的错误。这些错误并非异常,因此不受 [catch] 子句管理;
- 第 5 行:出现了在 [catch] 子句中定义的错误消息。因此,发生了源自 [\Exception] 的异常,或是源自 [\Error] 的错误。 这里我们可以看到,这是类 [\RuntimeException];
7.7. [catch] 子句的参数
让我们来分析以下 [exceptions-03.php] 脚本:
<?php
// 显示所有错误
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// 一个固定边界数组
$array = new \SplFixedArray(5);
try {
// 索引超出范围
$array[5] = 8;
} catch (\Throwable $ex) {
// 显示错误消息
print "Erreur 1 : " . $ex->getMessage() . "\n";
}
try {
// 指数超出范围
$array[5] = 8;
} catch (\Exception $ex) {
// 错误信息显示
print "Erreur 2 : " . $ex->getMessage() . "\n";
}
try {
// 指数超出范围
$array[5] = 8;
} catch (\RuntimeException $ex) {
// 显示错误信息
print "Erreur 3 : " . $ex->getMessage() . "\n";
}
try {
// 除以零
intdiv(5, 0);
} catch (\Throwable $ex) {
// 显示错误信息
print "Erreur 4 : " . $ex->getMessage() . "\n";
}
try {
// 除以 0
intdiv(5, 0);
} catch (\DivisionByzeroError $ex) {
// 显示错误信息
print "Erreur 5 : " . $ex->getMessage() . "\n";
}
try {
// 除以 0
intdiv(5, 0);
} catch (\Error $ex) {
// 显示错误信息
print "Erreur 6 : " . $ex->getMessage() . "\n";
}
try {
// 除以 0
intdiv(5, 0);
} catch (\Exception $ex) {
// 显示错误信息
print "Erreur 6 : " . $ex->getMessage() . "\n";
}
注释
- 第 8-31 行:处理因在 [\SplFixedArray] 类中使用错误索引而引发的异常的三种不同方法。我们已经看到,该错误会引发类型为 [RuntimeException] 的异常;
- 第 12 行:处理类型为 [\Throwable] 的错误。这是有效的,因为类型 [RuntimeException] 继承自类型 [\Exception],而后者实现了接口 [\Throwable];
- 第 20 行:处理类型为 [\Exception] 的错误。这是有效的,因为类型 [RuntimeException] 继承自类型 [\Exception];
- 第 28 行:处理类型为 [\RuntimeException] 的错误。这是首选的方法,因为它与生成的异常类型完全一致;
- 第 32-62 行:当向函数 [intdiv] 传入除数为 0 时,处理该函数生成的异常的 4 种不同方式。 函数 [ intdiv ( int $dividend , int $divisor ) : int] 执行整数除法 $dividend / $divisor。当除数为零时,将抛出异常 [\DivisionByzeroError];
- 第 35 行:拦截所有实现 [\Throwable] 接口的错误。这是有效的;
- 第43行:捕获错误的确切类型:这是首选的方法;
- 第51行:截获类型[\Error]。该类型有效,因为类[DivisionByzeroError]继承自类[Error];
- 第 59 行:截获类型 [\Exception]。该类型无效,因为类 [DivisionByzeroError] 与类 [\Exception] 没有任何关联;
结果
7.8. 子句 [finally]
try/catch 结构可以包含第三个元素,从而变成 try/catch/finally 结构。在以下两种情况下,[finally] 子句的代码都会被执行:
- [try]子句未抛出异常。此时该子句将完整执行,随后执行流程转至[finally]子句并完整执行;
- [try]子句抛出异常。此时该子句将执行至抛出异常的语句处。 随后,代码执行转至子句 [catch],该子句被完全执行。接着,代码执行转至子句 [finally],该子句被完全执行;
最后,[finally] 子句的代码仍会被执行。此场景在以下情况下非常有用:
- 在 [try] 中,代码已获取了资源(文件、数据库、网络连接、队列)。通常这些资源会占用大量内存。因此,应尽可能尽快释放(通常称为关闭)这些资源;
- 如果资源是在 [try] 中获取的,则应将其释放操作放在 [finally] 中。这确保了无论是否发生错误,已获取的资源都能被释放回系统;
以下脚本 [exemples/exceptions/exceptions-04.php] 展示了 [finally] 子句在各种情况下的运行机制:
<?php
// 或创建一个异常实例
$e = new \Exception("Erreur…");
var_dump($e);
// 第一次测试
try {
print "Premier test\n";
throw $e;
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
// 第二次测试
try {
print "Second test\n";
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
// 第三次测试
try {
print "Troisième test\n";
return;
} catch (\Exception $ex1) {
print $ex1->getMessage() . "\n";
} finally {
print "Terminé\n";
}
代码注释
- 第 4 行:$e 是预定义类 [\Exception] 的一个实例。我们将在不同位置调用它;
- 第 8-15 行:在 [try](第 10 行)中抛出了异常 $e;
- 第 11 行:捕获 [\Exception] 异常,并将错误消息写入控制台;
- 第13-15行:[finally]子程序写入一条消息。根据前文所述,无论[try]是否出错,该消息都应始终被写入;
- 第 18-24 行:[try] 中没有错误。此时也应进入 [finally];
- 第27-34行:try块中包含一条[return]语句且无错误。因此,我们可能会质疑是否会进入[finally]子句。执行结果表明确实会进入;
结果
让我们再看一个案例 [exceptions-05.php]:
<?php
// 第四次测试
try {
print "Quatrième test\n";
exit;
} finally {
print "Terminé\n";
}
注释
- 第 6 行:语句 [exit] 会立即终止脚本的执行:子句 [finally] 不会被执行;
- 第 4-9 行:一个不含 [catch] 子句的 try / catch / finally 示例。这是可行的;
结果
7.9. 创建自定义异常类
在规模稍大的项目中,将不同类型的错误封装到不同的异常类中进行区分是非常有用的。在前面的脚本中,我们看到任何异常都可以通过 [catch (\Throwable] 语句进行捕获。 如果对捕获到的错误一无所知,且所有错误的处理方式都相同,则建议采用这种做法。虽然有时确实如此,但通常我们需要根据错误的确切类型调整处理方式。因此,必须对错误进行区分。
让我们来看一下以下 [exceptions-06.php] 脚本:
<?php
// 定义我们自己的异常家族
class Exception1 extends \RuntimeException {
}
class Exception2 extends \RuntimeException {
}
// 或使用我们的异常
$e1 = new Exception1("Erreur1…");
var_dump($e1);
$e2 = new Exception2("Erreur2…");
var_dump($e2);
// 第一次测试
print ("premier test\n");
try {
// 抛出 Exception1 类型
throw $e1;
} catch (Exception1 $ex1) {
print "Exception 1" . "\n";
print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
print "Exception 2" . "\n";
print $ex2->getMessage() . "\n";
}
// 第二次测试
print ("second test\n");
try {
// 抛出 Exception2 类型
throw $e2;
} catch (Exception1 $ex1) {
print "Exception 1" . "\n";
print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
print "Exception 2" . "\n";
print $ex2->getMessage() . "\n";
}
// 第三次测试
print ("troisième test\n");
try {
// 抛出 Exception1 类型异常
throw $e1;
} catch (Exception1 | Exception2 $ex) {
print "Exception 1 ou 2" . "\n";
print $ex->getMessage() . "\n";
}
// 第四次测试
print ("quatrième test\n");
try {
// 抛出 Exception2 类型
throw $e2;
} catch (Exception1 | Exception2 $ex) {
print "Exception 1 ou 2" . "\n";
print $ex->getMessage() . "\n";
}
注释
- 第 4-10 行:定义了两个类 [Exception1] 和 [Exception2],它们都继承自预定义类 [\RuntimeException]。 这些类的类体为空。换言之,我们仅利用它们的类型:正因为它们具有不同的类型,我们才能在 [catch] 子句中区分这两个异常;
- 第13-16行:定义了两个变量$e1和$e2,它们分别具有类型[Exception1]和[Exception2];
- 第20-29行:采用try/catch/catch结构。这使得能够通过不同的[catch]子句来处理不同类型的异常;
- 第 23 行:捕获类型为 [Exception1] 的异常;
- 第 26 行:捕获类型为 [Exception2] 的异常;
- 第 49 行:捕获类型为 [Exception1] 或 (|) [Exception2] 的异常;
结果
结果说明
- 第 1-17 行:异常的“内容”:
- 第 2-3 行:错误消息;
- 第 6-7 行:错误代码;
- 第 8-9 行:发生异常的文件名;
- 第 10-11 行:发生异常的行号;
- 第 15-16 行:前一个异常。一个异常可以封装另一个异常,从而形成异常堆栈。属性 [previous] 将用于处理该堆栈;
7.10. 重新抛出异常
一个异常可以被多次抛出,如下面的脚本 [exceptions-07.php] 所示:
<?php
try {
try {
// 抛出一个异常
throw new \Exception("test");
} catch (\Exception $ex) {
// 重新抛出被拦截的异常
throw $ex;
} finally {
// 将顺利进入 finally 块
print "finally 1\n";
}
} catch (\Exception $ex2) {
// 成功捕获了初始异常
print $ex2->getMessage() . " dans try / catch / finally externe\n";
} finally {
// 将顺利进入 finally 块
print "finally 2\n";
}
注释
- 第 6 行:抛出一个异常;
- 第 7 行:捕获该异常;
- 第 9 行:重新抛出该异常。此时,它将进入上一级 try / catch / finally 结构;
- 第14行:再次捕获该异常;
- 第10-12行:执行结果表明,即使在第9行的[throw]之后,程序仍会进入try/catch/finally结构中的[finally]子句;
结果
7.11. 利用异常堆栈
一个异常可以封装另一个异常,而该异常又可以封装另一个异常,最终形成异常堆栈。以下是一个示例 [exceptions-08.php]:
注释
- 第 4-14 行:定义了三个从预定义异常 [RuntimeException] 派生的异常类;
- 第 17 行:[Exception3] 类的实例被封装在 [Exception2] 类的实例中,而该实例又被封装在 [Exception1] 类的实例中。 此处使用的构造函数是类 [Exception] 的构造函数:
![]()
构造函数的第三个参数用于封装异常。这在以下场景中可能很有用:
- 定义一个方法 M,该方法可能抛出类型为 [Exception1] 的异常,且仅抛出此类型(例如出于与接口兼容的考虑);
- 但在方法 M 中可能会发生其他类型的异常。为了将错误上报给调用方法 M 的代码,我们将这些异常封装到类型 [Exception1] 中并抛出该异常。这样可以避免丢失封装异常中所包含的信息,而这些信息正是导致错误的根本原因;
- 第20至27行展示了如何处理异常内部的异常堆栈;
结果
object(Exception1)#1 (7) {
["message":protected]=>
string(11) "Erreur 1…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(1)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
object(Exception2)#2 (7) {
["message":protected]=>
string(11) "Erreur 2…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(2)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
object(Exception3)#3 (7) {
["message":protected]=>
string(11) "Erreur 3…"
["string":"Exception":private]=>
string(0) ""
["code":protected]=>
int(0)
["file":protected]=>
string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
["line":protected]=>
int(17)
["trace":"Exception":private]=>
array(0) {
}
["previous":"Exception":private]=>
NULL
}
}
}
Erreur 1…
Erreur 2…
Erreur 3…