參考:
深入理解PHP原理之異常機制
我們什么時候應該使用異常
異常和錯誤
所有示例基於 PHP7。
應用中,關於錯誤的最佳實踐是:
- 必須報告錯誤
- 開發環境要顯示錯誤,生產環境不可顯示
- 開發環境和生產環境都要記錄錯誤日志
Error 和 Exception 的異同
- Exception 需要通過
throw new Exception
手動拋出 - Error 可以在 PHP 腳本執行發生錯誤時自動觸發,也可以通過
trigger_errors()
手動觸發 - 都實現了 Throwable 接口,可以通過
catch (Throwable $t) {...}
同時捕獲 Error 和 Exception - 如果不捕獲並處理 Exception,程序會終止,並報出 Fatal Error 錯誤,但捕獲后程序可以繼續執行
- 用
catch (Error $e) { ... }
,或者通過注冊錯誤處理函數( set_error_handler())來捕獲 Error - 用
catch (Exception $e) { ... }
或者通過注冊異常處理函數( set_exception_handler())來捕獲 Exception - 用
catch (Throwable $e) { ... }
可以同時捕獲 Exception 和 Error
<?php echo 1/0; echo 666; echo 1%0; echo 666;
PHP Warning: Division by zero in /code/main.php on line 3
INF666
PHP Fatal error: Uncaught DivisionByZeroError: Modulo by zero in /code/main.php:5
Throwable
用戶定義的類無法實現 Throwable,所以用戶只能拋出 Exception 或 Error 的實例。擴展 Throwable 的接口只能通過擴展 Exception 或 Error 的類來實現。
繼承關系
Error 類 和 Exception 類 都繼承自 Throwable 接口,不同版本的繼承關系可以參考 這里。下面是 7.2.0 - 7.2.7 的繼承關系:
Error
ArithmeticError
DivisionByZeroError
AssertionError
ParseError
TypeError
ArgumentCountError
Exception
ClosedGeneratorException
DOMException
ErrorException
IntlException
LogicException
BadFunctionCallException
BadMethodCallException
DomainException
InvalidArgumentException
LengthException
OutOfRangeException
PharException
ReflectionException
RuntimeException
OutOfBoundsException
OverflowException
PDOException
RangeException
UnderflowException
UnexpectedValueException
SodiumException
常用方法
完整的接口可以參考 這里。
- getMessage — 獲取異常消息內容
- getCode — 獲取異常代碼
- getFile — 導致異常的程序文件名稱
- getLine — 導致異常的行號
Error
從 PHP 7 開始,大多數錯誤(致命錯誤和可恢復錯誤)被作為 Error 異常拋出,從而可以捕獲並處理,防止腳本終止執行。與任何其他 Exception 異常一樣,可以使用 try / catch 塊捕獲 Error 對象。
從致命(fatal)和可恢復(recoverable)的錯誤中拋出的異常並沒有繼承 Exception,而是繼承自 Error。
Error 的嚴重等級
Parse error > Fatal Error > Waning > Notice > Deprecated
錯誤名稱 | 解釋 | 可能的原因 | 程序是否中止 | 如何捕獲錯誤 | 備注 |
---|---|---|---|---|---|
Parse error | 語法錯誤 | 代碼解析失敗 | 中斷執行 | PHP7 之后可以用 catch (Error $e) { ... } 捕獲 |
|
Fatal Error | 運行時錯誤 | 實例化不存在的類,調不存在的方法 | 中斷執行 | PHP7 之后可以用 catch (Error $e) { ... } 捕獲 |
可以使用 register_shutdown_function() 函數設置一個在 PHP 中止前執行收尾工作的函數 |
Waning | 警告 | 四則運算時出現非數字 | 繼續執行 | 可以用 set_error_handler() 捕獲 | |
Notice | 注意 | 變量或數組下標未定義 | 繼續執行 | 可以用 set_error_handler() 捕獲 | |
Deprecated | 使用了廢棄函數 | 函數已經廢棄 | 繼續執行 | 可以用 set_error_handler() 捕獲 |
Error 處理流程
- 先看看有沒有匹配的 catch 塊(注意是 Error 類型而不是 Exception 類型:
catch (Error $e) { ... }
),如果有則被第一個匹配的 try / catch 塊所捕獲。 - 如果沒有沒有匹配的 catch 塊,則去調用異常處理函數(事先通過 set_error_handler() 注冊)進行處理(僅用於 Deprecated、Notice、Waning 這三種級別)。
- 如果尚未注冊異常處理函數,則按照傳統方式處理:報告錯誤(Fatal Error 等)。
PHP 生成的每個錯誤都包含一個類型。類型列表以及它們的行為及其產生方式的簡短描述可以參考 這里。常用的有:
值 | 常量 | 說明 |
---|---|---|
1 | E_ERROR | 致命的運行時錯誤。不可捕捉,不可恢復。腳本終止運行。 |
2 | E_WARNING | 運行時警告 (非致命錯誤)。僅給出提示信息,但是腳本不會終止運行。 |
256 | E_USER_ERROR | 用戶產生的錯誤信息。類似 E_ERROR, 但是是由用戶自己在代碼中使用函數 trigger_error() 觸發的。 |
512 | E_USER_WARNING | 用戶產生的警告信息。類似 E_WARNING, 但是是由用戶自己在代碼中使用PHP函數 trigger_error() 觸發的。 |
2048 | E_STRICT (integer) | 啟用 PHP 對代碼的修改建議,以確保代碼具有最佳的互操作性和向前兼容性。 |
4096 | E_RECOVERABLE_ERROR | 可被捕捉的致命錯誤。它表示發生了一個可能非常危險的錯誤,但是還沒有導致 PHP 引擎處於不穩定的狀態。如果該錯誤沒有被用戶自定義處理程序捕獲(set_error_handler()),將成為一個 E_ERROR 從而腳本會終止運行。 |
8192 | E_DEPRECATED | 運行時通知。對在未來版本中可能無法正常工作的代碼給出警告。 |
30719 | E_ALL | E_STRICT 外的所有錯誤和警告信息。 |
設置 PHP 配置文件來處理錯誤
設置報告錯誤的等級
如果未設置錯誤處理程序,則 PHP 將根據 php.ini
配置文件處理發生的任何錯誤。error_reporting 指令控制報告和忽略哪些錯誤。雖然也可以在運行時通過調用 error_reporting() 函數來控制,但強烈建議設置配置指令,因為在腳本開始執行之前也可能會發生一些錯誤。
在開發環境中,為了了解並解決 PHP 引發的問題,最好將 error_reporting
設置為 E_ALL
來記錄所有的錯誤。生產環境中,可以將 error_reporting
設置為 E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
來避免記錄過多信息,但是在多數情況因為下 E_ALL
可以提供早期預警,記錄潛在的問題,也可以用於生產環境。
顯示錯誤或記錄日志
發生錯誤時,PHP 可以采取兩種措施,由另外兩個 php.ini
指令設置:
- display_errors:輸出錯誤。生產環境中必須禁用,因為它可能包含機密信息(如數據庫密碼),但可用於開發環境,確保立即報告問題。
- log_errors:記錄錯誤日志。這會將任何錯誤記錄到 error_log 定義的文件或 syslog 中。這在生產環境中非常有用,可以記錄發生的錯誤,然后根據這些錯誤生成報告。
用戶自定義錯誤處理程序
如果 PHP 的默認錯誤處理不滿足需求,還可以使用 set_error_handler() 安裝自己的自定義錯誤處理程序來處理許多類型的錯誤。
一般用於處理用戶通過 trigger_error 觸發的錯誤,大部分 PHP 內置錯誤類型無法以這種方式處理。可以按照腳本認為合適的方式處理那些可以處理的錯誤類型:例如,向用戶顯示自定義錯誤頁面,然后直接發送電子郵件報告錯誤,而不是通過日志。
set_error_handler('myErrorHandler');
function myErrorHandler($severity, $message, $filepath, $line) {
echo "錯誤信息:".$message;
// 發送電子郵件...
exit(1); // 必要時手動終止腳本
}
function myDiv($a, $b) {
return $a/$b;
}
myDiv(1, 0);
eval('ech 66'); // 無法用自定義的錯誤處理程序
將 Error 變為 ErrorException:
set_error_handler('myErrorHandler');
set_exception_handler('myExceptionHandler');
function myExceptionHandler($exception) {
echo $exception->getMessage();
}
function myErrorHandler($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) {
// This error code is not included in error_reporting, so let it fall
// through to the standard PHP error handler
return false;
}
throw new ErrorException($message, 0, $severity, $file, $line);
}
function myDiv($a, $b) {
return $a/$b;
}
myDiv(1, 0);
具體的 Error 類
ArithmeticError 算術錯誤
兩種可能的原因:
- 使用負數移位
- 調用
intdiv()
方法時,分子是PHP_INT_MIN
且分母為 -1(此時將返回浮點數)。
try {
$value = 1 << -1;
intdiv(PHP_INT_MIN, -1);
} catch (ArithmeticError $e) {
echo $e->getMessage(), "\n";
}
DivisionByZeroError
兩種可能的原因:
- 模數(%)運算時,分母為 0。
- 調用
intdiv()
方法時,分母為 0。
注意,在除法(/)運算符中使用零做分母僅發出警告。
try {
echo 1/0; // 僅警告
intdiv(1, 0);
echo 1%0;
} catch (DivisionByZeroError $e) {
echo $e->getMessage();
}
AssertionError
使用 assert() 語言結構進行斷言時,可能拋出這個錯誤:
ini_set('zend.assertions', 1); // 執行代碼
ini_set('assert.exception', 1); // 允許拋異常
$test = 1;
assert($test === 0);
ParseError
- 通過 included 或 required 引入文件有語法錯誤
eval()
解析的字符串有語法錯誤
try {
eval('ech 66');
include 'has-error.php';
} catch (ParseError $e) {
echo $e->getMessage(), "\n";
}
syntax error, unexpected '66' (T_LNUMBER)
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, called in D:\workspace\szhz\application\controllers\tuan\Index.php on line 312
Exception
Exception 出現的原因
PHP 在使用異常機制之前,通過返回錯誤碼來表示函數的執行結果。部分函數返回 TRUE 或 FALSE,部分函數返回 0 或 1、-1。難以統一且無法包含足夠的報錯原因等信息。例如 strtotime() 函數,成功則返回時間戳,否則返回 FALSE,但是在 PHP 5.1.0 之前本函數在失敗時返回 -1。
異常機制避免了錯誤碼機制的一些不足,可以在 一次捕獲多個異常。異常對象包含錯誤信息、錯誤碼、錯誤行號、文件、上下文,更方便定位問題。
Exception 特點
Exception 是必須手動拋出並且可被捕獲的。如果拋出的異常未被捕獲,則導致 Fatal error,並使得代碼停止執行。
function myDiv($a, $b) {
if ($b == 0)
throw new Exception('Divided by zero');
return $a/$b;
}
try {
myDiv(1, 0); // 如果不捕獲異常,則報錯 Fatal error,並停止執行
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
// 異常捕獲后,可以繼續執行后面的代碼
...
自定義 Exception
自定義的 Exception 需要繼承自已有異常,定義完成后就可以在代碼中拋出自定義的這些異常。
<?php class pdoDbException extends PDOException { public function __construct(PDOException $e) { if(strstr($e->getMessage(), 'SQLSTATE[')) { echo 'this is my exception'; } } } function f() { try { $pdo = new PDO('123.207.7.188', '$username', '$password', []); } catch (PDOException $e) { throw new pdoDbException($e); } } try { f(); } catch (pdoDbException $e) { print_r($e); }
用戶自定義異常處理程序
set_exception_handler('myExceptionHandler');
function myExceptionHandler($exception) {
echo $exception->getMessage();
}
function myDiv($a, $b) {
if ($b == 0)
throw new Exception('Divided by zero');
return $a/$b;
}
myDiv(1, 0);
// 自定義異常處理程序執行后,不會繼續執行后面的代碼