PHP(PHP_VERSION >= 7) 的 Error / Exception 的捕獲與處理仍是值得一說的,優雅處理錯誤與異常,在提高框架友好度的同時,也提高了開發效率。php
# 系統級用戶代碼的一些錯誤類型 可由 try ... catch ... 捕獲 E_PARSE 解析時錯誤 語法解析錯誤 少個分號 多個逗號一類的 致命錯誤 E_ERROR 運行時錯誤 好比調用了未定義的函數或方法 致命錯誤 # 系統級用戶代碼的一些錯誤類型 可由 set_error_handler 捕獲處理 E_WARNING 運行時警告 調用了未定義的變量 E_NOTICE 運行時提醒 E_DEPRECATED 運行時已廢棄的函數或方法 # 用戶級自定義錯誤 可由 trigger_error 觸發 可由 set_error_handler 捕獲處理 E_USER_ERROR 用戶自定義錯誤 致命錯誤 未處理也會致使程序退出 E_USER_WARNING E_USER_NOTICE E_USER_DEPRECATED ==========================開發中常遇到/不常遇到分割線======================= # Zend Engine 內部的一些錯誤 應該也能經過 try ... catch ... 捕獲 略難測試 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 的監聽級別設定。瀏覽器
<?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 標準錯誤處理是在一些錯誤沒有被用戶捕獲處理(沒有被 try ... catch ... 或 set_error_handler 捕獲處理)時,錯誤 會遞交至 PHP 標準錯誤處理。相關的設置項以下:函數
<?php // 監聽捕獲的錯誤級別 error_reporting(E_ALL); // 是否開啓錯誤信息回顯 將錯誤輸出至標準輸出(瀏覽器/命令行) ini_set('display_errors', true); // 死否開啓錯誤日誌記錄 將錯誤記錄至 ini:error_log 指定文件 ini_set('log_errors', true); ini_set('error_log', __DIR__ . '/php-errors.log');
獲取或設定當前錯誤的監聽級別。要注意,是獲取或設定的 PHP 標準錯誤處理 的級別,不會有效於 try...catch... 或 set_error_handler。測試
是否將錯誤信息回顯至標準輸出。默認開啓,生產環境下強烈建議 關閉 此項。ui
是否記錄錯誤日誌。默認關閉,生產環境下強烈建議 開啓 此項。編碼
錯誤日誌的保存文件。注意:若是路徑無效,display_errors 會被強制開啓。spa
set_error_handler 並不是能夠捕獲全部錯誤,且 set_error_handler 不會終止程序繼續執行。處理後若返回 false,則錯誤會被繼續遞交給 PHP 標準錯誤處理 流程。.net
能夠捕獲: 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 標準錯誤處理 是兩層錯誤捕捉器,有獨立的捕獲級別。
<?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; } // do some handle $error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line; echo $error . PHP_EOL; // 若是 return false 則錯誤會繼續遞交給 PHP 標準錯誤處理 // return false; }, 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);
trigger_error 用來觸發用戶級別的自定義錯誤,可使用 set_error_handler 捕獲處理。
默認的錯誤級別是 E_USER_NOTICE,咱們能夠自定義。
這裏須要注意的是:E_USER_ERROR 級別的錯誤若是被 PHP 標準錯誤處理 捕獲,腳本也會退出執行錯誤。
<?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;
set_exception_handler 用戶自定義捕獲異常 handler,異常沒有被 try ... catch 捕獲處理的話會被拋出,此時系統會檢查上下文是否註冊了 set_exception_handler。
若是未註冊 則進入 PHP 標準錯誤處理 致命錯誤退出執行。
若是已註冊 則進入 set_exception_handler 處理 程序依然會退出執行。
而 try ... catch ... 捕獲異常後仍不會退出執行,故強烈建議將有異常的執行邏輯放入 try ... catch 中
<?php // 捕獲異常後程序會退出執行 set_exception_handler(function ($exception) { echo $exception; // 此處程序會退出執行 異常到此結束 並不會交給 PHP 標準異常處理 }); throw new Exception('hello world!'); echo 'will i be executed?';
開發中用戶層面的 set_error_hanlder 沒法捕獲的錯誤還剩下 E_ERROR 和 E_PARSE 兩個級別,使用 try ... catch ... 則能夠將這倆貨捕捉到。
來個小插曲:你們對 E_PARSE 熟悉又陌生,可能常常遇到(各大框架都有此級別錯誤捕獲提示),但本身不知道如何捕獲,其實首先要理解 E_PARSE 錯誤的發生時段。
E_PARSE:即語法解析錯誤,Syntax Error then Parse Error,PHP 將腳本載入 Zend Engine 後,最開始要作的就是檢查基本語法是否有誤,無誤纔會調用解釋器,一行行的開始解釋運行。
這裏就有個雞生蛋,蛋生雞的問題了。以下代碼:
<?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 細化各種型的錯誤。
<?php // typeError demo function foo(): int { return 'hello world'; } try { //foo(); // echo strlen('hello world', 233); } 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) { // 除 0 沒法捕獲 但 除 0 取餘能夠捕獲 = = 很無奈 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 的錯誤。
固然,你也能夠顯示的判斷除數爲 0 來決定是否拋出個 DivisionByZeroError
<?php try { $divisor = 0; if ($divisor == 0) { throw new DivisionByZeroError('Division by zero!'); } echo 233 / $divisor; } catch (\DivisionByZeroError $error) { echo $error; }
Predefined Exceptions 預約義異常 可由系統自動拋出
Exception
ErrorException
Error
ArgumentCountError
ArithmeticError
AssertionError
DivisionByZeroError
ParseError
TypeError
SPL Exceptions SPL 標準規範異常 可供開發者規範代碼自行拋出
BadFunctionCallException
BadMethodCallException
DomainException
InvalidArgumentException
LengthException
LogicException
OutOfBoundsException
OutOfRangeException
OverflowException
RangeException
RuntimeException
UnderflowException
UnexpectedValueException
下面的代碼基本呈現和捕獲了 PHP7 提供的全部預約義錯誤和異常,PHP 標準錯誤處理
<?php // php >= 7 // PHP 標準錯誤處理 捕獲級別 error_reporting(E_ALL); // 是否將 標準錯誤處理 捕獲的錯誤回顯在 stdout 上 ini_set('display_errors', false); // 開啓錯誤日誌 ini_set('log_errors', true); // 若是錯誤日誌路徑無效 display_errors 依然會強制打開 ini_set('error_log', __DIR__ . '/php-errors.log'); /** * set_error_handler 用戶自定義錯誤 handler * 可以捕獲 E_WARNING E_NOTICE E_DEPRECATED E_USER_* E_STRICT 級的錯誤 * 沒法捕獲 E_ERROR E_PARSE E_CORE_* E_COMPILE_* [DivisionByZeroError TypeError] 級的錯誤 */ set_error_handler(function ($error_no, $error_msg, $error_file, $error_line) { switch ($error_no) { case E_WARNING: // x / 0 錯誤 PHP7 依然不能很友好的自動捕獲 只會產生 E_WARNING 級的錯誤 // 捕獲判斷後 throw new DivisionByZeroError($error_msg) // 或者使用 intdiv(x, 0) 方法 會自動拋出 DivisionByZeroError 的錯誤 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; } // do some handle $error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line; echo $error . PHP_EOL; // 若是 return false 則錯誤會繼續遞交給 PHP 標準錯誤處理 // return false; }, E_ALL | E_STRICT); /** * set_exception_handler 用戶自定義捕獲異常 handler * 異常沒有被 try ... catch ... 捕獲處理的話會被拋出 * 此時系統會檢查上下文是否註冊了 set_exception_handler * 若是未註冊 則進入 PHP 標準異常處理 致命錯誤退出執行 * 若是已註冊 則進入 set_exception_handler 處理 程序依然會退出執行 * 而 try ... catch ... 捕獲異常後仍不會退出執行 * 故強烈建議將有異常的執行邏輯放入 try ... catch 中 */ set_exception_handler(function ($exception) { echo $exception; // 此處程序會退出執行 異常到此結束 並不會交給 PHP 標準異常處理 }); // type error demo function foo(): int { return 'result type error'; } // 捕獲 E_ERROR E_PARSE 級的 Error // 捕獲 Exception try { // 加載外部文件的正確寫法 $file = __DIR__ . '/bar.inc.php'; if (file_exists($file)) { require_once $file; } else { throw new Exception($file . ' not exists!'); } // ParseError 解析錯誤 // bar.inc.php 的內容要有基本的語法錯誤: <?php echo 'some syntax error' // ArgumentCountError extends TypeError PHP >= 7.1.0 // strlen 參數錯誤 echo strlen('hello world', 4); // TypeError 類型錯誤 // foo 要求的返回類型爲 int 但 return 了 string 類型錯誤 foo(); // DivisionByZeroError extends ArithmeticError // x / 0 會拋出 E_WARNING 的異常 但不會自動拋出 DivisionByZeroError // 咱們可使用 set_error_handler 進行捕獲而後手動拋出 DivisionByZeroError 1 / 0; // Integer Divison 等同於 1 / 0 能夠直接拋出 DivisionByZeroError intdiv(1, 0); // 除 0 取餘 能夠直接拋出 DivisionByZeroError 1 % 0; // ArithmeticError 錯誤 intdiv(PHP_INT_MIN, -1); // AssertionError 斷言錯誤 assert('1 != 1'); // 調用未定義的函數 錯誤級別:E_ERROR 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 ) { // 傳參非法錯誤 php >= 7.1.0 echo 'Argument Count Error: ' . $argumentCountError . PHP_EOL; } catch (\TypeError $typeError) { // 類型錯誤 返回值 echo 'Type Error: ' . $typeError . PHP_EOL; } catch (\DivisionByZeroError $divisionByZeroError) { // x / 0 不拋出 x % 0 能夠拋出 // x / 0 能夠用 intdiv(x, 0) 代替 會拋出 echo 'Division By Zero Error: ' . $divisionByZeroError . PHP_EOL; } catch (\ArithmeticError $arithmeticError) { // 算數運算錯誤 intdiv(PHP_INT_MIN, -1) 觸發 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;
一、PHP 容許用戶自定義 Error 和 Exception 的捕獲與處理。如用戶未捕獲處理,則會遞交給 PHP 標準錯/異常處理,根據 errror_reporting display_errors log_erros error_log 參數決定處理方式。生產環境應關閉 display_errors 同時開啓 log_errors 記錄錯誤日誌。
二、set_error_handler 能夠捕獲 E_WARNING & E_NOTICE & E_DEPRECATED & E_USER_* 和 部分 E_STRICT 級的錯誤。set_error_handler 若是返回了 false 錯誤會遞交給 PHP 標準錯誤處理。set_error_handler 不會終止程序執行。
三、trigger_error 能夠用來拋出用戶級的錯誤,且 E_USER_ERROR 效用等同於 E_ERROR,PHP 標準錯誤處理 捕獲此級別的錯誤時會終止程序執行。
四、set_exception_handler 用戶自定義異常捕獲,捕獲後程序依然會終止運行,但不會再將異常遞交給 PHP 標準異常處理。
五、try ... catch 能夠捕獲全部的 Exception 和 E_ERROR & E_PARSE 級的錯誤。程序不會退出執行。
六、PHP 自帶了一些 Predefined Exceptions,同時有規範一些 SPL Exceptions,供開發者規範本身的錯誤異常架構。