PHP(PHP_VERSION >= 7) 的 Error / Exception 的捕获与处理还是值得一说的,优雅处理错误与异常,在提升框架友好度的同时,也提升了开发效率。
PHP 错误等级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| E_PARSE 解析时错误 语法解析错误 少个分号 多个逗号一类的 致命错误 E_ERROR 运行时错误 比如调用了未定义的函数或方法 致命错误
E_WARNING 运行时警告 调用了未定义的变量 E_NOTICE 运行时提醒 E_DEPRECATED 运行时已废弃的函数或方法
E_USER_ERROR 用户自定义错误 致命错误 未处理也会导致程序退出 E_USER_WARNING E_USER_NOTICE E_USER_DEPRECATED
==========================开发中常遇到/不常遇到分割线=======================
E_CORE_ERROR E_CORE_WARNING E_COMPILE_ERROR E_COMPILE_WARNING
E_STRICT 部分 try ... catch ... 部分 set_error_handler E_RECOVERABLE_ERROR
|
以上为 PHP 的一些错误监听级别,常用于 error_reporting 和 set_error_handler 的监听级别设定。
1 2 3 4 5
| <?php error_reporting(E_ALL & ~E_NOTICE);
set_error_handler(function handler($error_no, $error_msg, $error_file, $error_line) { }, 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 2 3 4 5 6 7 8 9 10
| <?php
error_reporting(E_ALL);
ini_set('display_errors', true);
ini_set('log_errors', true); 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <?php
set_error_handler(function ($error_no, $error_msg, $error_file, $error_line) { switch ($error_no) { case E_WARNING: $level_tips = 'PHP Warning: '; break; case E_NOTICE: $level_tips = 'PHP Notice: '; break; case E_DEPRECATED: $level_tips = 'PHP Deprecated: '; break; case E_USER_ERROR: $level_tips = 'User Error: '; break; case E_USER_WARNING: $level_tips = 'User Warning: '; break; case E_USER_NOTICE: $level_tips = 'User Notice: '; break; case E_USER_DEPRECATED: $level_tips = 'User Deprecated: '; break; case E_STRICT: $level_tips = 'PHP Strict: '; break; default: $level_tips = 'Unkonw Type Error: '; break; }
$error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line; echo $error . PHP_EOL;
}, E_ALL | E_STRICT);
trigger_error("用户自定义 notice error"); trigger_error("用户自定义 warning error", E_USER_WARNING); trigger_error("用户自定义 deprecated error", E_USER_DEPRECATED); trigger_error("用户自定义 fatal error", E_USER_ERROR);
|
2、trigger_error
trigger_error 用来触发用户级别的自定义错误,可以使用 set_error_handler 捕获处理。
默认的错误级别是 E_USER_NOTICE,我们可以自定义。
这里需要注意的是:E_USER_ERROR 级别的错误如果被 PHP 标准错误处理 捕获,脚本也会退出执行错误。
1 2 3 4 5 6 7 8 9 10 11 12
| <?php error_reporting(E_ALL); ini_set('display_errors', true);
trigger_error('用户自定义 notice error', E_USER_NOTICE); echo 'continue A' . PHP_EOL; trigger_error('用户自定义 warning error', E_USER_WARNING); echo 'continue B' . PHP_EOL; trigger_error('用户自定义 deprecated error', E_USER_DEPRECATED); echo 'continue C' . PHP_EOL; trigger_error('用户自定义 fatal error', E_USER_ERROR); 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 2 3 4 5 6 7 8 9 10
| <?php
set_exception_handler(function ($exception) { echo $exception; });
throw new Exception('hello world!');
echo 'will i be executed?';
|
4、try … catch …
开发中用户层面的 set_error_hanlder 无法捕获的错误还剩下 E_ERROR 和 E_PARSE 两个级别,使用 try … catch … 则可以将这俩货捕捉到。
来个小插曲:大家对 E_PARSE 熟悉又陌生,可能经常遇到(各大框架都有此级别错误捕获提示),但自己不知道如何捕获,其实首先要理解 E_PARSE 错误的发生时段。
E_PARSE:即语法解析错误,Syntax Error then Parse Error,PHP 将脚本载入 Zend Engine 后,最开始要做的就是检查基本语法是否有误,无误才会调用解释器,一行行的开始解释运行。
这里就有个鸡生蛋,蛋生鸡的问题了。如下代码:
1 2 3 4 5
| <?php //认为关闭了错误捕获 不应该有错误报出才对 error_reporting(0);
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?php
function foo(): int { return 'hello world'; }
try { } catch (\ErrorException $errorException) { echo 'ErrorException: ' . $errorException . PHP_EOL; } catch (\Exception $exception) { echo 'Exception: ' . $exception . PHP_EOL; } catch (\TypeError $typeError) { echo 'Type Error: ' . $typeError . PHP_EOL; } catch (\ParseError $parseError) { echo 'Parse Error: ' . $parseError . PHP_EOL; } catch (\DivisionByZeroError $divisionByZeroError) { echo 'Division By Zero Error: ' . $divisionByZeroError . PHP_EOL; } catch (\Error $error) { echo 'Error: ' . $error . PHP_EOL; }
|
这里要注意的是,DivisionByZeroError 在 PHP7 中依然无法隐式的完美捕获。准确的说:
当 x / 0 时抛出一个 E_WARNING 级别的错误,我们可以 set_error_handler 捕获,然后再判断错误为
‘Devision by zero’ 时抛出一个 ErrorException 的异常交由 try … catch … 捕获处理即可。
当 x % 0 时才会直接抛出 DivisionByZeroError 的错误。
或者使用 intdiv(x, 0) 来代替 x / 0,会自动抛出 DivisionByZeroError
当然,你也可以显示的判断除数为 0 来决定是否抛出个 DivisionByZeroError
1 2 3 4 5 6 7 8 9 10
| <?php try { $divisor = 0; if ($divisor == 0) { throw new DivisionByZeroError('Division by zero!'); } echo 233 / $divisor; } catch (\DivisionByZeroError $error) { echo $error; }
|
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
| <?php
error_reporting(E_ALL);
ini_set('display_errors', false);
ini_set('log_errors', true);
ini_set('error_log', __DIR__ . '/php-errors.log');
set_error_handler(function ($error_no, $error_msg, $error_file, $error_line) { switch ($error_no) { case E_WARNING: if (strcmp('Division by zero', $error_msg) == 0) { throw new \DivisionByZeroError($error_msg); }
$level_tips = 'PHP Warning: '; break; case E_NOTICE: $level_tips = 'PHP Notice: '; break; case E_DEPRECATED: $level_tips = 'PHP Deprecated: '; break; case E_USER_ERROR: $level_tips = 'User Error: '; break; case E_USER_WARNING: $level_tips = 'User Warning: '; break; case E_USER_NOTICE: $level_tips = 'User Notice: '; break; case E_USER_DEPRECATED: $level_tips = 'User Deprecated: '; break; case E_STRICT: $level_tips = 'PHP Strict: '; break; default: $level_tips = 'Unkonw Type Error: '; break; }
$error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line; echo $error . PHP_EOL;
}, E_ALL | E_STRICT);
set_exception_handler(function ($exception) { echo $exception; });
function foo(int $bar): int { return 'result type error'; }
try { $file = __DIR__ . '/bar.inc.php'; if (file_exists($file)) { require_once $file; } else { throw new Exception($file . ' not exists!'); }
echo strlen('hello world', 4);
foo("type error");
1 / 0; intdiv(1, 0); 1 % 0;
intdiv(PHP_INT_MIN, -1);
assert('1 != 1');
bar(); } catch (\ErrorException $errorException) { echo 'ErrorException: ' . $errorException . PHP_EOL; } catch (\Exception $exception) { echo 'Exception: ' . $exception . PHP_EOL; } catch (\ParseError $parseError) { echo 'Parse Error: ' . $parseError . PHP_EOL; } catch (\ArgumentCountError $argumentCountError ) { echo 'Argument Count Error: ' . $argumentCountError . PHP_EOL; } catch (\TypeError $typeError) { echo 'Type Error: ' . $typeError . PHP_EOL; } catch (\DivisionByZeroError $divisionByZeroError) { echo 'Division By Zero Error: ' . $divisionByZeroError . PHP_EOL; } catch (\ArithmeticError $arithmeticError) { echo 'Arithmetic Error: ' . $arithmeticError . PHP_EOL; } catch (\AssertionError $assertionError) { echo 'Assertion Error: ' . $assertionError . PHP_EOL; } catch (\Error $error) { echo 'Error: ' . $error . PHP_EOL; }
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,供开发者规范自己的错误异常架构。
转载自 https://segmentfault.com/a/1190000014977430