PHP 完善的 Error / Exception 的捕获与处理

转载自:https://blog.csdn.net/ivan820819/article/details/80470807

PHP(PHP_VERSION >= 7) 的 Error / Exception 的捕获与处理还是值得一说的,优雅处理错误与异常,在提升框架友好度的同时,也提升了开发效率。

PHP 错误等级


    
    
  1. # 系统级用户代码的一些错误类型 可由 try ... catch ... 捕获
  2. E_PARSE 解析时错误 语法解析错误 少个分号 多个逗号一类的 致命错误
  3. E_ERROR 运行时错误 比如调用了未定义的函数或方法 致命错误
  4. # 系统级用户代码的一些错误类型 可由 set_error_handler 捕获处理
  5. E_WARNING 运行时警告 调用了未定义的变量
  6. E_NOTICE 运行时提醒
  7. E_DEPRECATED 运行时已废弃的函数或方法
  8. # 用户级自定义错误 可由 trigger_error 触发 可由 set_error_handler 捕获处理
  9. E_USER_ERROR 用户自定义错误 致命错误 未处理也会导致程序退出
  10. E_USER_WARNING
  11. E_USER_NOTICE
  12. E_USER_DEPRECATED
  13. ==========================开发中常遇到/不常遇到分割线=======================
  14. # Zend Engine 内部的一些错误 应该也能通过 try ... catch ... 捕获 略难测试
  15. E_CORE_ERROR
  16. E_CORE_WARNING
  17. E_COMPILE_ERROR
  18. E_COMPILE_WARNING
  19. #编码标准化警告(建议如何修改以向前兼容)
  20. E_STRICT 部分 try ... catch ... 部分 set_error_handler
  21. E_RECOVERABLE_ERROR

以上为 PHP 的一些错误监听级别,常用于 error_reporting 和 set_error_handler 的监听级别设定。


    
    
  1. <?php
  2. error_reporting(E_ALL & ~E_NOTICE);
  3. set_error_handler( function handler($error_no, $error_msg, $error_file, $error_line) {
  4. }, E_ALL | E_STRICT);

PHP 的错误处理其实可以分为:用户自定义错误处理PHP标准错误处理,两者的关系相当于两层错误捕捉器,系统会先检测是否定义了 用户自定义错误处理,否则会将错误交由 PHP标准错误处理 进行处理。

注意:PHP 的所有的 Exception 都属于 E_ERROR 级的错误,抛出时如果没有被捕获而交由 PHP 标准错误处理的话,就会 Fatal Error 导致程序退出执行。当然,PHP7 为了细化错误级别,划分了 Error 级 Error 的衍生类,这些也都属于 E_ERROR 级别的错误。

PHP 标准错误处理

PHP 标准错误处理是在一些错误没有被用户捕获处理(没有被 try ... catch ... 或 set_error_handler 捕获处理)时,错误 会递交至 PHP 标准错误处理。相关的设置项如下:


    
    
  1. <?php
  2. // 监听捕获的错误级别
  3. error_reporting(E_ALL);
  4. // 是否开启错误信息回显 将错误输出至标准输出(浏览器/命令行)
  5. ini_set( 'display_errors' , true );
  6. // 死否开启错误日志记录 将错误记录至 ini:error_log 指定文件
  7. ini_set( 'log_errors' , true );
  8. ini_set( 'error_log' , __DIR__ . '/php-errors.log' );

1、error_reporting([level])

获取或设定当前错误的监听级别。要注意,是获取或设定的 PHP 标准错误处理 的级别,不会有效于 try...catch... 或 set_error_handler。

2、display_errors

是否将错误信息回显至标准输出。默认开启,生产环境下强烈建议 关闭 此项。

3、log_errors

是否记录错误日志。默认关闭,生产环境下强烈建议 开启 此项。

4、error_log

错误日志的保存文件。注意:如果路径无效,display_errors 会被强制开启。

PHP 用户自定义错误处理

1、set_error_handler

set_error_handler 并非可以捕获所有错误,且 set_error_handler 不会终止程序继续执行。处理后若返回 false,则错误会被继续递交给 PHP 标准错误处理 流程。

可以捕获: E_WARNING & E_NOTICE & E_DEPRCATED & E_USER_* & 部分 E_STRICT 级的错误。
无法捕获: E_ERROR & E_PARSE & E_CORE_* & E_COMPLIE_* 级的错误。

有自身的错误捕获级别,默认E_ALL | E_STRICT,且不受 error_reporting 设定的级别的影响。这里要理解,用户自定义错误处理 和 PHP 标准错误处理 是两层错误捕捉器,有独立的捕获级别。


    
    
  1. <?php
  2. // 用户自定义错误处理
  3. set_error_handler( function ($error_no, $error_msg, $error_file, $error_line) {
  4. switch ($error_no) {
  5. case E_WARNING:
  6. $level_tips = 'PHP Warning: ' ;
  7. break ;
  8. case E_NOTICE:
  9. $level_tips = 'PHP Notice: ' ;
  10. break ;
  11. case E_DEPRECATED:
  12. $level_tips = 'PHP Deprecated: ' ;
  13. break ;
  14. case E_USER_ERROR:
  15. $level_tips = 'User Error: ' ;
  16. break ;
  17. case E_USER_WARNING:
  18. $level_tips = 'User Warning: ' ;
  19. break ;
  20. case E_USER_NOTICE:
  21. $level_tips = 'User Notice: ' ;
  22. break ;
  23. case E_USER_DEPRECATED:
  24. $level_tips = 'User Deprecated: ' ;
  25. break ;
  26. case E_STRICT:
  27. $level_tips = 'PHP Strict: ' ;
  28. break ;
  29. default :
  30. $level_tips = 'Unkonw Type Error: ' ;
  31. break ;
  32. }
  33. // do some handle
  34. $error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line;
  35. echo $error . PHP_EOL;
  36. // 如果 return false 则错误会继续递交给 PHP 标准错误处理
  37. // return false;
  38. }, E_ALL | E_STRICT);
  39. trigger_error( "用户自定义 notice error" );
  40. trigger_error( "用户自定义 warning error" , E_USER_WARNING);
  41. trigger_error( "用户自定义 deprecated error" , E_USER_DEPRECATED);
  42. trigger_error( "用户自定义 fatal error" , E_USER_ERROR);

2、trigger_error

trigger_error 用来触发用户级别的自定义错误,可以使用 set_error_handler 捕获处理。
默认的错误级别是 E_USER_NOTICE,我们可以自定义。
这里需要注意的是:E_USER_ERROR 级别的错误如果被 PHP 标准错误处理 捕获,脚本也会退出执行错误。


    
    
  1. <?php
  2. error_reporting(E_ALL);
  3. ini_set( 'display_errors' , true );
  4. trigger_error( '用户自定义 notice error' , E_USER_NOTICE);
  5. echo 'continue A' . PHP_EOL;
  6. trigger_error( '用户自定义 warning error' , E_USER_WARNING);
  7. echo 'continue B' . PHP_EOL;
  8. trigger_error( '用户自定义 deprecated error' , E_USER_DEPRECATED);
  9. echo 'continue C' . PHP_EOL;
  10. trigger_error( '用户自定义 fatal error' , E_USER_ERROR);
  11. echo 'D point will not be executed' . PHP_EOL;

3、set_exception_handler

set_exception_handler 用户自定义捕获异常 handler,异常没有被 try ... catch 捕获处理的话会被抛出,此时系统会检查上下文是否注册了 set_exception_handler。
如果未注册 则进入 PHP 标准错误处理 致命错误退出执行。
如果已注册 则进入 set_exception_handler 处理 程序依然会退出执行。
而 try ... catch ... 捕获异常后仍不会退出执行,故强烈建议将有异常的执行逻辑放入 try ... catch 中


    
    
  1. <?php
  2. // 捕获异常后程序会退出执行
  3. set_exception_handler( function ($exception) {
  4. echo $exception;
  5. // 此处程序会退出执行 异常到此结束 并不会交给 PHP 标准异常处理
  6. });
  7. throw new Exception ( 'hello world!' );
  8. echo 'will i be executed?' ;

4、try ... catch ...

开发中用户层面的 set_error_hanlder 无法捕获的错误还剩下 E_ERRORE_PARSE 两个级别,使用 try ... catch ... 则可以将这俩货捕捉到。

来个小插曲:大家对 E_PARSE 熟悉又陌生,可能经常遇到(各大框架都有此级别错误捕获提示),但自己不知道如何捕获,其实首先要理解 E_PARSE 错误的发生时段。

E_PARSE:即语法解析错误,Syntax Error then Parse Error,PHP 将脚本载入 Zend Engine 后,最开始要做的就是检查基本语法是否有误,无误才会调用解释器,一行行的开始解释运行。

这里就有个鸡生蛋,蛋生鸡的问题了。如下代码:


     
     
  1. <?php
  2. //认为关闭了错误捕获 不应该有错误报出才对
  3. error_reporting( 0 );
  4. echo 'i lose semicolon'

然后很多人会诧异,明明关闭了错误提示,为什么还会报错?

没错,代码的确正确的书写了关闭错误提示的意图。但还没被执行到时,脚本就因为最初始的语法解析错误,被 Zend Engine 抛出 Parse Error 终止执行了。同时还要理解,PHP include / require 只有在真的解释到这一行代码时,引用的文件才会被载入--解析--解释执行。

所以,我们需要有一个无错的 try ... catch ... 容器,在容器中便可以对后续引用的外部脚本进行 E_PARSE 错误捕捉。

例如框架自身是一个无错的运行容器,开发者自写的 MVC 是被 include / require 到此容器中 解析 -- 解释执行 的,用户代码的语法错误即会被容器的 try ... catch ... 优雅的捕获到。

try ... catch ... 的错误捕获级别同样不受 error_reporting 影响,我们可以通过 多层 catch 细化各类型的错误。


    
    
  1. <?php
  2. // typeError demo
  3. function foo(): int
  4. {
  5. return 'hello world' ;
  6. }
  7. try {
  8. //foo();
  9. // echo strlen('hello world', 233);
  10. } catch (\ErrorException $errorException) {
  11. // 捕获错误异常
  12. echo 'ErrorException: ' . $errorException . PHP_EOL;
  13. } catch (\ Exception $exception) {
  14. // 捕获异常
  15. echo 'Exception: ' . $exception . PHP_EOL;
  16. } catch (\TypeError $typeError) {
  17. // 捕获类型错误 返回值/参数不正确
  18. echo 'Type Error: ' . $typeError . PHP_EOL;
  19. } catch (\ParseError $parseError) {
  20. // 捕获解析错误 语法错误
  21. echo 'Parse Error: ' . $parseError . PHP_EOL;
  22. } catch (\DivisionByZeroError $divisionByZeroError) {
  23. // 除 0 无法捕获 但 除 0 取余可以捕获 = = 很无奈
  24. echo 'Division By Zero Error: ' . $divisionByZeroError . PHP_EOL;
  25. } catch (\Error $error) {
  26. // 基本错误
  27. echo 'Error: ' . $error . PHP_EOL;
  28. }

这里要注意的是,DivisionByZeroError 在 PHP7 中依然无法隐式的完美捕获。准确的说:

当 x / 0 时抛出一个 E_WARNING 级别的错误,我们可以 set_error_handler 捕获,然后再判断错误为
'Devision by zero' 时抛出一个 ErrorException 的异常交由 try ... catch ... 捕获处理即可。

当 x % 0 时才会直接抛出 DivisionByZeroError 的错误。

当然,你也可以显示的判断除数为 0 来决定是否抛出个 DivisionByZeroError


    
    
  1. <?php
  2. try {
  3. $divisor = 0 ;
  4. if ($divisor == 0 ) {
  5. throw new DivisionByZeroError( 'Division by zero!' );
  6. }
  7. echo 233 / $divisor;
  8. } catch (\DivisionByZeroError $error) {
  9. echo $error;
  10. }

PHP 预定义的 Error 和 Exception

Predefined Exceptions 预定义异常 可由系统自动抛出
http://php.net/manual/en/rese...

Exception
ErrorException
Error
ArgumentCountError
ArithmeticError
AssertionError
DivisionByZeroError
ParseError
TypeError

SPL Exceptions SPL 标准规范异常 可供开发者规范代码自行抛出
http://php.net/manual/en/spl....

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

完善的错误和异常捕获

下面的代码基本呈现和捕获了 PHP7 提供的所有预定义错误和异常 及 PHP 标准错误处理
1、使用 try ... catch 捕获 E_ERROR 及 E_PARSE 级别的 Error (及Error 类的衍生类) 和 Exception (ErrorException)。
2、对于 try ... catch 无法捕获的 E_WARNING,E_NOTICE,E_DEPRECATED,E_USER_*,部分 E_STRICTED 级别的错误,我们使用 set_error_handler 捕获处理,捕获后我们其实可以将错误信息封装到 ErrorException 中并抛出,这样处理流又会交给 try ... catch,可以统一处理,比如Yii2框架就是这样处理的。
3、set_exception_handler 则是捕获在没有 try ... catch 中执行的代码的异常,所以强烈建议一些存在异常 风险的逻辑要放入 try ... catch 中。


    
    
  1. <?php
  2. // php >= 7
  3. // PHP 标准错误处理 捕获级别
  4. error_reporting(E_ALL);
  5. // 是否将 标准错误处理 捕获的错误回显在 stdout 上
  6. ini_set( 'display_errors' , false );
  7. // 开启错误日志
  8. ini_set( 'log_errors' , true );
  9. // 如果错误日志路径无效 display_errors 依然会强制打开
  10. ini_set( 'error_log' , __DIR__ . '/php-errors.log' );
  11. /**
  12. * set_error_handler 用户自定义错误 handler
  13. * 能够捕获 E_WARNING E_NOTICE E_DEPRECATED E_USER_* E_STRICT 级的错误
  14. * 无法捕获 E_ERROR E_PARSE E_CORE_* E_COMPILE_* [DivisionByZeroError TypeError] 级的错误
  15. */
  16. set_error_handler( function ($error_no, $error_msg, $error_file, $error_line) {
  17. switch ($error_no) {
  18. case E_WARNING:
  19. // x / 0 错误 PHP7 依然不能很友好的自动捕获 只会产生 E_WARNING 级的错误
  20. // 捕获判断后 throw new DivisionByZeroError($error_msg)
  21. // 或者使用 intdiv(x, 0) 方法 会自动抛出 DivisionByZeroError 的错误
  22. if (strcmp( 'Division by zero' , $error_msg) == 0 ) {
  23. throw new \DivisionByZeroError($error_msg);
  24. }
  25. $level_tips = 'PHP Warning: ' ;
  26. break ;
  27. case E_NOTICE:
  28. $level_tips = 'PHP Notice: ' ;
  29. break ;
  30. case E_DEPRECATED:
  31. $level_tips = 'PHP Deprecated: ' ;
  32. break ;
  33. case E_USER_ERROR:
  34. $level_tips = 'User Error: ' ;
  35. break ;
  36. case E_USER_WARNING:
  37. $level_tips = 'User Warning: ' ;
  38. break ;
  39. case E_USER_NOTICE:
  40. $level_tips = 'User Notice: ' ;
  41. break ;
  42. case E_USER_DEPRECATED:
  43. $level_tips = 'User Deprecated: ' ;
  44. break ;
  45. case E_STRICT:
  46. $level_tips = 'PHP Strict: ' ;
  47. break ;
  48. default :
  49. $level_tips = 'Unkonw Type Error: ' ;
  50. break ;
  51. }
  52. // do some handle
  53. $error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line;
  54. echo $error . PHP_EOL;
  55. // or throw a ErrorException back to try ... catch block
  56. // throw new \ErrorException($error);
  57. // 如果 return false 则错误会继续递交给 PHP 标准错误处理
  58. // return false;
  59. }, E_ALL | E_STRICT);
  60. /**
  61. * set_exception_handler 用户自定义捕获异常 handler
  62. * 异常没有被 try ... catch ... 捕获处理的话会被抛出
  63. * 此时系统会检查上下文是否注册了 set_exception_handler
  64. * 如果未注册 则进入 PHP 标准异常处理 致命错误退出执行
  65. * 如果已注册 则进入 set_exception_handler 处理 程序依然会退出执行
  66. * 而 try ... catch ... 捕获异常后仍不会退出执行
  67. * 故强烈建议将有异常的执行逻辑放入 try ... catch 中
  68. */
  69. set_exception_handler( function ($exception) {
  70. echo $exception;
  71. // 此处程序会退出执行 异常到此结束 并不会交给 PHP 标准异常处理
  72. });
  73. // type error demo
  74. function foo(): int
  75. {
  76. return 'result type error' ;
  77. }
  78. // 捕获 E_ERROR E_PARSE 级的 Error
  79. // 捕获 Exception
  80. try {
  81. // 加载外部文件的正确写法
  82. $file = __DIR__ . '/bar.inc.php' ;
  83. if (file_exists($file)) {
  84. require_once $file;
  85. } else {
  86. throw new Exception ($file . ' not exists!' );
  87. }
  88. // ParseError 解析错误
  89. // bar.inc.php 的内容要有基本的语法错误: <?php echo 'some syntax error'
  90. // ArgumentCountError extends TypeError PHP >= 7.1.0
  91. // strlen 参数错误
  92. echo strlen( 'hello world' , 4 );
  93. // TypeError 类型错误
  94. // foo 要求的返回类型为 int 但 return 了 string 类型错误
  95. foo();
  96. // DivisionByZeroError extends ArithmeticError
  97. // x / 0 会抛出 E_WARNING 的异常 但不会自动抛出 DivisionByZeroError
  98. // 我们可以使用 set_error_handler 进行捕获然后手动抛出 DivisionByZeroError
  99. 1 / 0 ;
  100. // Integer Divison 等同于 1 / 0 可以直接抛出 DivisionByZeroError
  101. intdiv( 1 , 0 );
  102. // 除 0 取余 可以直接抛出 DivisionByZeroError
  103. 1 % 0 ;
  104. // ArithmeticError 错误
  105. intdiv(PHP_INT_MIN, -1 );
  106. // AssertionError 断言错误
  107. assert( '1 != 1' );
  108. // 调用未定义的函数 错误级别:E_ERROR
  109. bar();
  110. } catch (\ErrorException $errorException) {
  111. // 错误异常
  112. // 最常用的就是将那几个非致命的错误捕获后 ErrorException 回抛到 try ... catch 中
  113. echo 'ErrorException: ' . $errorException . PHP_EOL;
  114. } catch (\ Exception $exception) {
  115. // 异常
  116. echo 'Exception: ' . $exception . PHP_EOL;
  117. } catch (\ParseError $parseError) {
  118. // 解析错误 语法错误
  119. echo 'Parse Error: ' . $parseError . PHP_EOL;
  120. } catch (\ArgumentCountError $argumentCountError ) {
  121. // 传参非法错误 php >= 7.1.0
  122. echo 'Argument Count Error: ' . $argumentCountError . PHP_EOL;
  123. } catch (\TypeError $typeError) {
  124. // 类型错误 返回值
  125. echo 'Type Error: ' . $typeError . PHP_EOL;
  126. } catch (\DivisionByZeroError $divisionByZeroError) {
  127. // x / 0 不抛出 x % 0 可以抛出
  128. // x / 0 可以用 intdiv(x, 0) 代替 会抛出
  129. echo 'Division By Zero Error: ' . $divisionByZeroError . PHP_EOL;
  130. } catch (\ArithmeticError $arithmeticError) {
  131. // 算数运算错误 intdiv(PHP_INT_MIN, -1) 触发
  132. echo 'Arithmetic Error: ' . $arithmeticError . PHP_EOL;
  133. } catch (\AssertionError $assertionError) {
  134. // 断言错误
  135. echo 'Assertion Error: ' . $assertionError . PHP_EOL;
  136. } catch (\Error $error) {
  137. // 基本错误
  138. echo 'Error: ' . $error . PHP_EOL;
  139. }
  140. echo "run finished!" . PHP_EOL;

总结

1、PHP 允许用户自定义 Error 和 Exception 的捕获与处理。如用户未捕获处理,则会递交给 PHP 标准错/异常处理,根据 errror_reporting display_errors log_erros error_log 参数决定处理方式。生产环境应关闭 display_errors 同时开启 log_errors 记录错误日志。

2、set_error_handler 可以捕获 E_WARNING & E_NOTICE & E_DEPRECATED & E_USER_* 和 部分 E_STRICT 级的错误。set_error_handler 如果返回了 false 错误会递交给 PHP 标准错误处理。set_error_handler 不会终止程序执行。

3、trigger_error 可以用来抛出用户级的错误,且 E_USER_ERROR 效用等同于 E_ERROR,PHP 标准错误处理 捕获此级别的错误时会终止程序执行。

4、set_exception_handler 用户自定义异常捕获,捕获后程序依然会终止运行,但不会再将异常递交给 PHP 标准异常处理。

5、try ... catch 可以捕获所有的 Exception 和 E_ERROR & E_PARSE 级的错误。程序不会退出执行。

6、PHP 自带了一些 Predefined Exceptions,同时有规范一些 SPL Exceptions,供开发者规范自己的错误异常架构。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值