前段時間在項目中遇到一個當時覺得比較奇怪的情況:使用 GuzzleHttp 發(fā)送 curl 請求,API 響應(yīng)超時導(dǎo)致拋出異常。但 catch(Exception) 并沒有捕獲異常,導(dǎo)致代碼意外停止運行。后來查資料發(fā)現(xiàn),在 PHP 7 中,GuzzleHttp 請求超時拋出的異常繼承的是 Error,而 Error 并沒有繼承 Exception,所以 catch(Exception) 無法捕獲并處理該異常。
PHP 7 中對 Error 的處理
在 PHP 5 中,當程序中有致命錯誤發(fā)生時,腳本會立即停止運行。并且,通過 set_error_handler 設(shè)置的錯誤處理程序在這種情況下并不會被調(diào)用。
【推薦學(xué)習(xí):PHP7教程】
⒈ 自定義錯誤處理程序 set_error_handler
??set_error_handler 接受兩個參數(shù),第一個為自定義的錯誤處理函數(shù),第二個參數(shù)指定觸發(fā)該自定義錯誤處理函數(shù)的錯誤級別。但需要指出的是,在任何時候,只能有一個自定義的錯誤處理程序起作用。
function func_notice($num, $str, $file, $line) { print "Encountered notice $num in $file, line $line: $strn"; } function func_error($num, $str, $file, $line) { print "Encountered error $num in $file, line $line: $strn"; } set_error_handler("func_notice", E_NOTICE); set_error_handler("func_error", E_ERROR); echo $foo;
??以上代碼在執(zhí)行以后,會輸出 PHP Notice: Undefined variable: foo 。在第二個 set_error_handler 執(zhí)行以后,自定義錯誤處理函數(shù)變成了 func_error ,同時,觸發(fā)自定義錯誤處理函數(shù)的錯誤級別變成了 E_ERROR 。而在 PHP 中,變量未定義只會觸發(fā) E_NOTICE 級別的錯誤,所以自定義的錯誤處理函數(shù)并不會被觸發(fā)。
需要指出的是,自定義的錯誤處理函數(shù)對以下幾種錯誤級別并不起作用:
E_ERROR、E_PARSE、E_CORE_ERROR、E_CORE_WARNING、E_COMPILE_ERROR、E_COMPILE_WARNING、E_STRICT
??在上述幾種自定義錯誤處理程序無法處理的錯誤中,凡是以 ERROR 結(jié)尾的都是致命錯誤。其他幾種雖然不是致命錯誤,但
-
E_PARSE 是在解析 PHP 代碼時產(chǎn)生的錯誤,此時 PHP 代碼尚未開始運行,自定義錯誤處理程序自然無法處理該錯誤
-
E_CORE_WARNING 產(chǎn)生于 PHP 的初始化啟動階段,此時 PHP 代碼仍然尚未運行,所以不能被自定義錯誤處理程序處理
-
E_COMPILE_WARNING 是在 PHP 代碼的編譯階段產(chǎn)生,所以不能被自定義錯誤處理程序處理
而至于 E_STRICT 是 PHP 為了保證代碼的最佳互操作性和向前兼容而提出的代碼修改建議,自然也不會被自定義錯誤處理函數(shù)處理
function func_error($num, $str, $file, $line) { print "Encountered error $num in $file, line $line: $strn"; } set_error_handler('func_error', E_NOTICE); $obj = 'foo'; $obj->method();
?? 以上代碼運行輸出結(jié)果:
PHP Fatal error: Call to a member function method() on string
??雖然設(shè)置了自定義錯誤處理程序,但在致命錯誤發(fā)生時,并不起作用。
??對于這種自定義錯誤處理程序無法處理的致命錯誤,在 PHP 5 中可以通過注冊一個終止回調(diào)(shutdown_function)來記錄具體的錯誤信息,但也僅限于記錄錯誤信息,當發(fā)生致命錯誤時代碼仍然會停止運行。
$shutdownHandler = function(){ print PHP_EOL; print "============================" . PHP_EOL; print "Running the shutdown handler" . PHP_EOL; $error = error_get_last(); if (!empty($error)) { print "Looks like there was an error: " . print_r($error, true) . PHP_EOL; // 可以添加記錄日志的邏輯 } else { // 程序正常運行結(jié)束 print "Running a normal shutdown without error." . PHP_EOL; } }; register_shutdown_function($shutdownHandler); $obj = 'foo'; $obj->method();
??以上代碼執(zhí)行會輸出
PHP Fatal error: Call to a member function method() on string in /home/chenyan/test.php on line 24 ============================ Running the shutdown handler Looks like there was an error: Array ( [type] => 1 [message] => Call to a member function method() on string [file] => /home/chenyan/test.php [line] => 24 )
⒉ 撤銷自定義錯誤處理程序
??當同時設(shè)置多個自定義錯誤處理程序時,雖然只有最后設(shè)置的自定義錯誤處理程序起作用。但所有設(shè)置的自定義錯誤處理程序會以棧的方式保存(FILO)。
??使用 restore_error_handler 可以撤銷最近一次設(shè)置的自定義錯誤處理程序;如果同時調(diào)用了多次 set_error_handler ,則每調(diào)用一次 restore_error_handler,處于棧頂?shù)腻e誤處理程序就會被撤銷。
function func_notice($num, $str, $file, $line) { print "Encountered notice : $strn"; } set_error_handler("func_notice", E_NOTICE); set_error_handler("func_notice", E_NOTICE); set_error_handler("func_notice", E_NOTICE); echo $foo; set_error_handler("func_notice", E_NOTICE); echo $foo; restore_error_handler(); echo $foo; restore_error_handler(); echo $foo; restore_error_handler(); echo $foo; restore_error_handler(); echo $foo;
??以上代碼運行,會輸出:
Encountered notice : Undefined variable: foo Encountered notice : Undefined variable: foo Encountered notice : Undefined variable: foo Encountered notice : Undefined variable: foo Encountered notice : Undefined variable: foo PHP Notice: Undefined variable: foo
⒊ PHP 7 中對錯誤的處理
??在 PHP 7 中,當有致命錯誤或 E_RECOVERABLE_ERROR 類型的錯誤發(fā)生時,通常會拋出一個 Error,程序并不會終止。
try { $obj = 'foo'; $obj->method(); } catch (Error $e) { echo $e->getMessage(); }
??運行以上代碼會輸出
Call to a member function method() on string
E_RECOVERABLE_ERROR 是一種可捕獲的致命錯誤,這種錯誤的出現(xiàn)并不會使得 Zend 引擎處于不穩(wěn)定的狀態(tài),但必須被捕獲并且處理。如果不處理,那么這種錯誤最終會變成 E_ERROR 類型的錯誤,最終導(dǎo)致 PHP 代碼停止運行。
??php 7 中,并不是所有的致命錯誤都會拋出 Error,一些特定情況下出現(xiàn)的致命錯誤( Out Of Memory)仍然會導(dǎo)致代碼停止運行。另外,如果拋出的 Error 沒有被捕獲并處理,則代碼仍然會停止運行。
// bak.sql 的大小為 377 M // PHP 配置的 memory_limit = 128M try { $file = './bak.sql'; file_get_contents($file); } catch (Error $e) { echo $e->getMessage(); } // 執(zhí)行以上代碼,仍然會產(chǎn)生致命錯誤 PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 395191240 bytes) // 拋出的 Error 沒有被捕獲并處理,代碼依然會停止運行 $obj = 'foo'; $obj->method(); // 執(zhí)行以上代碼,由于并沒有用 try/catch 捕獲并處理拋出的 Error,程序仍然會停止運行 PHP Fatal error: Uncaught Error: Call to a member function method() on string
??PHP 7 中的 Error 并沒有繼承 Exception,之所以這樣做是為了防止 PHP 5 中捕獲并處理 Exception 的代碼捕獲這些 Error。因為在 PHP 5 中,這些致命錯誤是會導(dǎo)致代碼停止運行的。
??Error 和 Exception 都繼承自 Throwable 。在 PHP 7 中,Throwable 是一個 interface,所有能通過 throw 關(guān)鍵字拋出的對象都實現(xiàn)了這個 interface。
interface Throwable { public function getMessage(): string; public function getCode(): int; public function getFile(): string; public function getLine(): int; public function getTrace(): array; public function getTraceAsString(): string; public function getPrevious(): Throwable; public function __toString(): string; }
??需要指出的是,Throwable 是 PHP 底層的 interface,PHP 代碼中不能直接實現(xiàn) Throwable 。之所以作出這個限制,是因為通常只有 Error 和 Exception 可以被拋出,并且這些拋出的 Error 和 Exception 中還存儲了它們被拋出的堆棧跟蹤信息,而 PHP 代碼中開發(fā)者自定義的 class 無法實現(xiàn)這些。
??要在 PHP 代碼中實現(xiàn) Throwable 必須通過繼承 Exception 來實現(xiàn)。
interface CustomThrowable extends Throwable {} class CustomException extends Exception implements CustomThrowable {} throw new CustomException();
??PHP 7 中 Error 和 Exception 的繼承關(guān)系
interface Throwable |- Exception implements Throwable |- Other Exception classes |- Error implements Throwable |- TypeError extends Error |- ParseError extends Error |- AssertionError extends Error |- ArithmeticError extends Error |- DivizionByZeroError extends ArithmeticError
-
TypeError
??當函數(shù)的傳參或返回值的數(shù)據(jù)類型與申明的數(shù)據(jù)類型不一致時,會拋出 TypeError
function add(int $left, int $right) { return $left + $right; } try { $value = add('left', 'right'); } catch (TypeError $e) { echo $e->getMessage(); } // 運行以上代碼,會輸出: Argument 1 passed to add() must be of the type int, string given
??當開啟嚴格模式時,如果 PHP 內(nèi)建函數(shù)的傳參個數(shù)與要求的參數(shù)不一致,也會拋出 TypeError
declare(strict_types = 1); try { substr('abc'); } catch (TypeError $e) { echo $e->getMessage(); } // 運行以上代碼,會輸出: substr() expects at least 2 parameters, 1 given
??默認情況下,PHP 7 處于弱模式。在弱模式下,PHP 7 會盡可能的將傳參的數(shù)據(jù)類型轉(zhuǎn)換為期望的數(shù)據(jù)類型。例如,如果函數(shù)期望的參數(shù)類型為 string,而實際傳參的數(shù)據(jù)類型的 int,那么 PHP 會把 int 轉(zhuǎn)換為 string。
// declare(strict_types = 1); function add(string $left, string $right) { return $left + $right; } try { $value = add(11, 22); echo $value; } catch (TypeError $e) { echo $e->getMessage(); } // 以上代碼運行,會正常輸出 33,PHP 會對傳參的數(shù)據(jù)類型做轉(zhuǎn)換(int→string→int) // 但如將 PHP 改為嚴格模式,則運行是會拋出 TypeError Argument 1 passed to add() must be of the type string, int given
-
ParseError
??當在 include 或 require 包含的文件中存在語法錯誤,或 eval() 函數(shù)中的代碼中存在語法錯誤時,會拋出 ParseError
// a.php $a = 1 $b = 2 // test.php try { require 'a.php'; } catch (ParseError $e) { echo $e->getMessage(); } // 以上代碼運行會輸出: syntax error, unexpected '$b' (T_VARIABLE) // eval 函數(shù)中的代碼存在語法錯誤 try { eval("$a = 1"); } catch (ParseError $e) { echo $e->getMessage(); } // 以上代碼運行會輸出: syntax error, unexpected end of file
-
AssertionError
??當斷言失敗時,會拋出 AssertionError(此時要求 PHP 配置中 zend.assertions = 1,assert.exception = 1,這兩個配置可以在 php.ini 文件中配置,也可以通過 ini_set() 在 PHP 代碼中配置)。
ini_set('zend_assertions', 1); ini_set('assert.exception', 1); try { $test = 1; assert($test === 0); } catch (AssertionError $e) { echo $e->getMessage(); } // 運行以上代碼會輸出: assert($test === 0)
-
ArithmeticError
??在 PHP 7 中,目前有兩種情況會拋出 ArithmeticError:按位移動操作,第二個參數(shù)為負數(shù);使用 intdiv() 函數(shù)計算 PHP_INT_MIN 和 -1 的商(如果使用 / 計算 PHP_INT_MIN 和 -1 的商,結(jié)果會自動轉(zhuǎn)換為 float 類型)。
try { $value = 1 << -1; } catch (ArithmeticError $e) { echo $e->getMessage(); } // 運行以上代碼,會輸出: Bit shift by negative number try { $value = intdiv(PHP_INT_MIN, -1); } catch (ArithmeticError $e) { echo $e->getMessage(); } // 運行以上代碼,會輸出: Division of PHP_INT_MIN by -1 is not an integer
-
DivisionByZeroError
??拋出 DivisionByZeorError 的情況目前也有兩種:在進行取模(%)運算時,第二個操作數(shù)為 0;使用 intdiv() 計算兩個數(shù)的商時,除數(shù)為 0。如果使用 / 計算兩個數(shù)的商時除數(shù)為 0,PHP 只會產(chǎn)生一個 Warning。并且,如果被除數(shù)非 0,則結(jié)果為 INF,如果被除數(shù)也是 0,則結(jié)果為 NaN。
try { $value = 1 % 0; echo $value; } catch (DivisionByZeroError $e) { echo $e->getMessage(), "n"; } // 運行以上代碼,會輸出: Modulo by zero try { $value = intdiv(0, 0); echo $value; } catch (DivisionByZeroError $e) { echo $e->getMessage(), "n"; } // 運行以上代碼,會輸出: Division by zero
??通常在實際的業(yè)務(wù)中,捕獲并處理拋出的 Error 并不常見,因為一旦拋出 Error 說明代碼存在嚴重的 BUG,需要修復(fù)。所以,在實際的業(yè)務(wù)中,Error