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。
