PHP 中的 Exception, Error, Throwable
- PHP 中將代碼自身異常(一般是環境或者語法非法所致)稱作錯誤
Error
,將運行中出現的邏輯錯誤稱為異常Exception
- 錯誤是沒法通過代碼處理的,而異常則可以通過
try/catch
來處理 - PHP7 中出現了
Throwable
接口,該接口由Error
和Exception
實現,用戶不能直接實現Throwable
接口,而只能通過繼承Exception
來實現接口
PHP7 異常處理機制
過去的 PHP,處理致命錯誤幾乎是不可能的。致命錯誤不會調用由 set_error_handler()
設置的處理方式,而是簡單的停止腳本的執行。
在 PHP7 中,當致命錯誤和可捕獲的錯誤(E_ERROR
和 E_RECOVERABLE_ERROR
)發生時會拋出異常,而不是直接停止腳本的運行。對於某些情況,比如內存溢出,致命錯誤則仍然像之前一樣直接停止腳本執行。在 PHP7 中,一個未捕獲的異常也會是一個致命錯誤。這意味着在 PHP5.x 中致命錯誤拋出的異常未捕獲,在 PHP7 中也是致命錯誤。
注意:其他級別的錯誤如warning
和notice
,和之前一樣不會拋出異常,只有fatal
和recoverable
級別的錯誤會拋出異常。
從 fatal
和 recoverable
級別錯誤拋出的異常並非繼承自 Exception
類。這種分離是為了防止現有 PHP5.x 的用於停止腳本運行的代碼也捕獲到錯誤拋出的異常。fatal
和 recoverable
級別的錯誤拋出的異常是一個全新分離出來的類 Error
類的實例。跟其他異常一樣,Error
類異常也能被捕獲和處理,同樣允許在 finally
之類的塊結構中運行。
Throwable
為了統一兩個異常分支,Exception
和 Error
都實現了一個全新的接口:Throwable
PHP7 中新的異常結構如下:
1 interface Throwable 2 |- Exception implements Throwable 3 |- ... 4 |- Error implements Throwable 5 |- TypeError extends Error 6 |- ParseError extends Error 7 |- ArithmeticError extends Error 8 |- DivisionByZeroError extends ArithmeticError 9 |- AssertionError extends Error
如果在 PHP7 的代碼中定義了 Throwable
類,它將會是如下這樣:
1 interface Throwable{ 2 public function getMessage(): string; 3 public function getCode(): int; 4 public function getFile(): string; 5 public function getLine(): int; 6 public function getTrace(): array; 7 public function getTraceAsString(): string; 8 public function getPrevious(): Throwable; 9 public function __toString(): string; 10 }
這個接口看起來很熟悉。Throwable
規定的方法跟 Exception
幾乎是一樣的。唯一不同的是 Throwable::getPrevious()
返回的是 Throwable
的實例而不是 Exception
的。Exception
和 Error
的構造函數跟之前 Exception
一樣,可以接受任何 Throwable
的實例。
Throwable
可以用於 try/catch
塊中捕獲 Exception
和 Error
對象(或是任何未來可能的異常類型)。記住捕獲更多特定類型的異常並且對之做相應的處理是更好的實踐。然而在某種情況下我們想捕獲任何類型的異常(比如日志或框架中錯誤處理)。在 PHP7 中,要捕獲所有的應該使用 Throwable
而不是 Exception
。
1 try { 2 // Code that may throw an Exception or Error. 3 } catch (Throwable $t) { 4 // Handle exception 5 }
用戶定義的類不能實現 Throwable
接口。做出這個決定一定程度上是為了預測性和一致性——只有 Exception
和 Error
的對象可以被拋出。此外,異常需要攜帶對象在追溯堆棧中創建位置的信息,而用戶定義的對象不會自動的有參數來存儲這些信息。
Throwable
可以被繼承從而創建特定的包接口或者添加額外的方法。一個繼承自 Throwable
的接口只能被 Exception
或 Error
的子類來實現。
1 interface MyPackageThrowable extends Throwable {} 2 3 class MyPackageException extends Exception implements MyPackageThrowable {} 4 5 throw new MyPackageException();
Error
事實上,PHP5.x 中所有的錯誤都是 fatal
或 recoverable
級別的錯誤,在 PHP7 中都能拋出一個 Error
實例。跟其他任何異常一樣,Error
對象可以使用 try/catch
塊來捕獲。
1 $var = 1; 2 try { 3 $var->method(); // Throws an Error object in PHP 7. 4 } catch (Error $e) { 5 // Handle error 6 }
通常情況下,之前的致命錯誤都會拋出一個基本的 Error
類實例,但某些錯誤會拋出一個更具體的 Error
子類:TypeError
、ParseError
以及 AssertionError
。
TypeError
當函數參數或返回值不符合聲明的類型時,TypeError
的實例會被拋出。
1 function add(int $left, int $right){ 2 return $left + $right; 3 } 4 5 try { 6 $value = add('left', 'right'); 7 } catch (TypeError $e) { 8 echo $e->getMessage(), "\n"; 9 } 10 11 //Argument 1 passed to add() must be of the type integer, string given
ParseError
當 include/require
文件或 eval()
代碼存在語法錯誤時,ParseError
會被拋出。
1 try { 2 require 'file-with-parse-error.php'; 3 } catch (ParseError $e) { 4 echo $e->getMessage(), "\n"; 5 }
ArithmeticError
ArithmeticError
在兩種情況下會被拋出。一是位移操作負數位。二是調用intdiv()
時分子是 PHP_INT_MIN
且分母是 -1 (這個使用除法運算符的表達式:PHP_INT_MIN / -1
,結果是浮點型)。
1 try { 2 $value = 1 << -1; 3 catch (ArithmeticError $e) { 4 echo $e->getMessage();//Bit shift by negative number 5 }
DevisionByZeroError
當 intdiv()
的分母是 0 或者取模操作 (%) 中分母是 0 時,DivisionByZeroError
會被拋出。注意在除法運算符 (/) 中使用 0 作除數(也即xxx/0這樣寫)時只會觸發一個 warning,這時候若分子非零結果是 INF,若分子是 0 結果是 NaN。
1 try { 2 $value = 1 % 0; 3 } catch (DivisionByZeroError $e) { 4 echo $e->getMessage();//Modulo by zero 5 }
AssertionError
當 assert()
的條件不滿足時,AssertionError
會被拋出。
ini_set('zend.assertions', 1);
1 ini_set('assert.exception', 1); 2 3 $test = 1; 4 5 assert($test === 0); 6 7 //Fatal error: Uncaught AssertionError: assert($test === 0)
只有斷言啟用並且是設置 ini 配置的 zend.assertions = 1
和 assert.exception = 1
時,assert()
才會執行並拋 AssertionError
。
在你的代碼中使用 Error
用戶可以通過繼承 Error
來創建符合自己層級要求的 Error
類。這就形成了一個問題:什么情況下應該拋出 Exception
,什么情況下應該拋出 Error
。
Error
應該用來表示需要程序員關注的代碼問題。從 PHP 引擎拋出的 Error
對象屬於這些分類,通常都是代碼級別的錯誤,比如傳遞了錯誤類型的參數給一個函數或者解析一個文件發生錯誤。Exception
則應該用於在運行時能安全的處理,並且另一個動作能繼續執行的情況。
由於 Error
對象不應該在運行時被處理,因此捕獲 Error
對象也應該是不頻繁的。一般來說,Error
對象僅被捕獲用於日志記錄、執行必要的清理以及展示錯誤信息給用戶。
編寫代碼支持 PHP5.x 和 PHP7 的異常
為了在同樣的代碼中捕獲任何 PHP5.x 和 PHP7 的異常,可以使用多個 catch
,先捕獲 Throwable
,然后是 Exception
。當 PHP5.x 不再需要支持時,捕獲 Exception
的 catch
塊可以移除。
1 try { 2 // Code that may throw an Exception or Error. 3 } catch (Throwable $t) { 4 // Executed only in PHP 7, will not match in PHP 5.x 5 } catch (Exception $e) { 6 // Executed only in PHP 5.x, will not be reached in PHP 7 7 }
不幸的是,處理異常的函數中的類型聲明不容易確定。當 Exception
用於函數參數類型聲明時,如果函數調用時候能用 Error
的實例,這個類型聲明就要去掉。當 PHP5.x 不需要被支持時,類型聲明則可以還原為 Throwable
。