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 會被強制開啓。.net
set_error_handler 並不是能夠捕獲全部錯誤,且 set_error_handler 不會終止程序繼續執行。處理後若返回 false,則錯誤會被繼續遞交給 PHP 標準錯誤處理 流程。命令行
能夠捕獲: 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) { // 捕獲類型錯誤 返回值/參數類型不正確 declare(strict_types = 1) 嚴格模式下更容易出現 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 的錯誤。
或者使用 intdiv(x, 0) 來代替 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 預約義異常 可由系統自動拋出
http://php.net/manual/en/rese...
Exception
ErrorException
Error
ArgumentCountError
ArithmeticError
AssertionError
DivisionByZeroError
ParseError
TypeError
SPL Exceptions SPL 標準規範異常 可供開發者規範代碼自行拋出
http://php.net/manual/en/spl....
BadFunctionCallException
BadMethodCallException
DomainException
InvalidArgumentException
LengthException
LogicException
OutOfBoundsException
OutOfRangeException
OverflowException
RangeException
RuntimeException
UnderflowException
UnexpectedValueException
下面的代碼基本呈現和捕獲了 PHP7 提供的全部預約義錯誤和異常 及 PHP 標準錯誤處理
一、使用 try ... catch 捕獲 E_ERROR 及 E_PARSE 級別的 Error (及Error 類的衍生類) 和 Exception (ErrorException)。
二、對於 try ... catch 沒法捕獲的 E_WARNING,E_NOTICE,E_DEPRECATED,E_USER_*,部分 E_STRICTED 級別的錯誤,咱們使用 set_error_handler 捕獲處理,捕獲後咱們其實能夠將錯誤信息封裝到 ErrorException 中並拋出,這樣處理流又會交給 try ... catch,能夠統一處理,好比Yii2框架就是這樣處理的。
三、set_exception_handler 則是捕獲在沒有 try ... catch 中執行的代碼的異常,因此強烈建議一些存在異常 風險的邏輯要放入 try ... catch 中。
<?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; // or throw a ErrorException back to try ... catch block // throw new \ErrorException($error); // 若是 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 $bar): 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("type error"); // 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) { // 錯誤異常 // 最經常使用的就是將那幾個非致命的錯誤捕獲後 ErrorException 回拋到 try ... catch 中 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,供開發者規範本身的錯誤異常架構。