Skip to content

7. 异常与错误

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

7.1. 脚本树

Image

7.2. 接口 [\Throwable]

接口 [\Throwable] 如下:

Image

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

Image

7.3. PHP 7 中预定义的异常

PHP 7 定义了多个异常类:

Image

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

7.4. PHP 7 中预定义的错误

PHP 7 定义了多个错误类:

Image

[\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 报告所有错误。第二个参数是所需的错误级别:

Image

Image

  • 第5行:要求将错误显示在控制台上;
  • 第 9 行:访问了数组 [$var] 中不存在的元素;
  • 第 11 行:进行了除以零的操作;
  • 第14行:创建类[SplFixedArray]的实例。该类用于创建具有固定边界和整数索引的数组;
  • 第 18 行:访问数组中不存在的元素;

结果

1
2
3
4
5
6
7
8
9
Notice: Undefined index: abcd in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php on line 9

Warning: Division by zero in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php on line 11
float(INF)

Fatal error: Uncaught RuntimeException: Index invalid or out of range in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php:18
Stack trace:
#0 {main}
thrown in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php on line 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
2
3
4
5
Notice: Undefined index: abcd in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-02.php on line 10

Warning: Division by zero in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-02.php on line 12
float(INF)
erreur, message : Index invalid or out of range, type : RuntimeException

结果说明

  • 第 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] 没有任何关联;

结果

Erreur 1 : Index invalid or out of range
Erreur 2 : Index invalid or out of range
Erreur 3 : Index invalid or out of range
Erreur 4 : Division by zero
Erreur 5 : Division by zero
Erreur 6 : Division by zero

Fatal error: Uncaught DivisionByZeroError: Division by zero in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-03.php:58
Stack trace:
#0 C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-03.php(58): intdiv(5, 0)
#1 {main}
thrown in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-03.php on line 58

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]子句。执行结果表明确实会进入;

结果

1
2
3
4
5
6
7
Premier test
Erreur…
Terminé
Second test
Terminé
Troisième test
Terminé

让我们再看一个案例 [exceptions-05.php]


<?php

// 第四次测试
try {
  print "Quatrième test\n";
  exit;
} finally {
  print "Terminé\n";
}

注释

  • 第 6 行:语句 [exit] 会立即终止脚本的执行:子句 [finally] 不会被执行;
  • 第 4-9 行:一个不含 [catch] 子句的 try / catch / finally 示例。这是可行的;

结果

Quatrième test

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] 的异常;

结果

object(Exception1)#1 (7) {
  ["message":protected]=>
  string(10) "Erreur1…"
  ["string":"Exception":private]=>
  string(0) ""
  ["code":protected]=>
  int(0)
  ["file":protected]=>
  string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-06.php"
  ["line":protected]=>
  int(13)
  ["trace":"Exception":private]=>
  array(0) {
  }
  ["previous":"Exception":private]=>
  NULL
}
object(Exception2)#2 (7) {
  ["message":protected]=>
  string(10) "Erreur2…"
  ["string":"Exception":private]=>
  string(0) ""
  ["code":protected]=>
  int(0)
  ["file":protected]=>
  string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-06.php"
  ["line":protected]=>
  int(15)
  ["trace":"Exception":private]=>
  array(0) {
  }
  ["previous":"Exception":private]=>
  NULL
}
premier test
Exception 1
Erreur1…
second test
Exception 2
Erreur2…
troisième test
Exception 1 ou 2
Erreur1…
quatrième test
Exception 1 ou 2
Erreur2…

结果说明

  • 第 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]子句;

结果

1
2
3
finally 1
test dans try / catch / finally externe
finally 2

7.11. 利用异常堆栈

一个异常可以封装另一个异常,而该异常又可以封装另一个异常,最终形成异常堆栈。以下是一个示例 [exceptions-08.php]

<?php

// 定义我们自己的异常家族
class Exception1 extends \RuntimeException {

}

class Exception2 extends \RuntimeException {

}

class Exception3 extends \RuntimeException {

}

// 或使用我们的异常
$e1 = new Exception1("Erreur 1…", 1, new Exception2("Erreur 2…", 2, new Exception3("Erreur 3…")));
var_dump($e1);
// 处理当前异常
print $e1->getMessage() . "\n";
$e = $e1;
while ($e->getPrevious() !== NULL) {
  // 前一个异常
  $e = $e->getPrevious();
  // 错误消息
  print $e->getMessage() . "\n";
}

注释

  • 第 4-14 行:定义了三个从预定义异常 [RuntimeException] 派生的异常类;
  • 第 17 行:[Exception3] 类的实例被封装在 [Exception2] 类的实例中,而该实例又被封装在 [Exception1] 类的实例中。 此处使用的构造函数是类 [Exception] 的构造函数:

Image

构造函数的第三个参数用于封装异常。这在以下场景中可能很有用:

  • 定义一个方法 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…