PHP 中 Error 和 Exception 兩種異常的特性及日志記錄或顯示


PHP 文檔:
Error
Exception

參考:
深入理解PHP原理之異常機制
我們什么時候應該使用異常
異常和錯誤

所有示例基於 PHP7。

應用中,關於錯誤的最佳實踐是:

  • 必須報告錯誤
  • 開發環境要顯示錯誤,生產環境不可顯示
  • 開發環境和生產環境都要記錄錯誤日志

Error 和 Exception 的異同

  • Exception 需要通過 throw new Exception 手動拋出
  • Error 可以在 PHP 腳本執行發生錯誤時自動觸發,也可以通過 trigger_errors() 手動觸發
  • 都實現了 Throwable 接口,可以通過 catch (Throwable $t) {...} 同時捕獲 Error 和 Exception
  • 如果不捕獲並處理 Exception,程序會終止,並報出 Fatal Error 錯誤,但捕獲后程序可以繼續執行
  • catch (Error $e) { ... },或者通過注冊錯誤處理函數( set_error_handler())來捕獲 Error
  • catch (Exception $e) { ... } 或者通過注冊異常處理函數( set_exception_handler())來捕獲 Exception
  • catch (Throwable $e) { ... } 可以同時捕獲 Exception 和 Error
<?php echo 1/0; echo 666; echo 1%0; echo 666;
PHP Warning: Division by zero in /code/main.php on line 3
INF666
PHP Fatal error:  Uncaught DivisionByZeroError: Modulo by zero in /code/main.php:5

Throwable

用戶定義的類無法實現 Throwable,所以用戶只能拋出 Exception 或 Error 的實例。擴展 Throwable 的接口只能通過擴展 Exception 或 Error 的類來實現。

繼承關系

Error 類 和 Exception 類 都繼承自 Throwable 接口,不同版本的繼承關系可以參考 這里。下面是 7.2.0 - 7.2.7 的繼承關系:

Error
   ArithmeticError
      DivisionByZeroError
   AssertionError
   ParseError
   TypeError
      ArgumentCountError
Exception
   ClosedGeneratorException
   DOMException
   ErrorException
   IntlException
   LogicException
      BadFunctionCallException
         BadMethodCallException
      DomainException
      InvalidArgumentException
      LengthException
      OutOfRangeException
   PharException
   ReflectionException
   RuntimeException
      OutOfBoundsException
      OverflowException
      PDOException
      RangeException
      UnderflowException
      UnexpectedValueException
   SodiumException

常用方法

完整的接口可以參考 這里

  • getMessage — 獲取異常消息內容
  • getCode — 獲取異常代碼
  • getFile — 導致異常的程序文件名稱
  • getLine — 導致異常的行號

Error

從 PHP 7 開始,大多數錯誤(致命錯誤和可恢復錯誤)被作為 Error 異常拋出,從而可以捕獲並處理,防止腳本終止執行。與任何其他 Exception 異常一樣,可以使用 try / catch 塊捕獲 Error 對象。

從致命(fatal)和可恢復(recoverable)的錯誤中拋出的異常並沒有繼承 Exception,而是繼承自 Error。

Error 的嚴重等級

Parse error > Fatal Error > Waning > Notice > Deprecated

錯誤名稱 解釋 可能的原因 程序是否中止 如何捕獲錯誤 備注
Parse error 語法錯誤 代碼解析失敗 中斷執行 PHP7 之后可以用 catch (Error $e) { ... } 捕獲
Fatal Error 運行時錯誤 實例化不存在的類,調不存在的方法 中斷執行 PHP7 之后可以用 catch (Error $e) { ... } 捕獲 可以使用 register_shutdown_function() 函數設置一個在 PHP 中止前執行收尾工作的函數
Waning 警告 四則運算時出現非數字 繼續執行 可以用 set_error_handler() 捕獲
Notice 注意 變量或數組下標未定義 繼續執行 可以用 set_error_handler() 捕獲
Deprecated 使用了廢棄函數 函數已經廢棄 繼續執行 可以用 set_error_handler() 捕獲

Error 處理流程

  1. 先看看有沒有匹配的 catch 塊(注意是 Error 類型而不是 Exception 類型:catch (Error $e) { ... }),如果有則被第一個匹配的 try / catch 塊所捕獲。
  2. 如果沒有沒有匹配的 catch 塊,則去調用異常處理函數(事先通過 set_error_handler() 注冊)進行處理(僅用於 Deprecated、Notice、Waning 這三種級別)。
  3. 如果尚未注冊異常處理函數,則按照傳統方式處理:報告錯誤(Fatal Error 等)。

PHP 生成的每個錯誤都包含一個類型。類型列表以及它們的行為及其產生方式的簡短描述可以參考 這里。常用的有:

常量 說明
1 E_ERROR 致命的運行時錯誤。不可捕捉,不可恢復。腳本終止運行。
2 E_WARNING 運行時警告 (非致命錯誤)。僅給出提示信息,但是腳本不會終止運行。
256 E_USER_ERROR 用戶產生的錯誤信息。類似 E_ERROR, 但是是由用戶自己在代碼中使用函數 trigger_error() 觸發的。
512 E_USER_WARNING 用戶產生的警告信息。類似 E_WARNING, 但是是由用戶自己在代碼中使用PHP函數 trigger_error() 觸發的。
2048 E_STRICT (integer) 啟用 PHP 對代碼的修改建議,以確保代碼具有最佳的互操作性和向前兼容性。
4096 E_RECOVERABLE_ERROR 可被捕捉的致命錯誤。它表示發生了一個可能非常危險的錯誤,但是還沒有導致 PHP 引擎處於不穩定的狀態。如果該錯誤沒有被用戶自定義處理程序捕獲(set_error_handler()),將成為一個 E_ERROR 從而腳本會終止運行。
8192 E_DEPRECATED 運行時通知。對在未來版本中可能無法正常工作的代碼給出警告。
30719 E_ALL E_STRICT 外的所有錯誤和警告信息。

設置 PHP 配置文件來處理錯誤

設置報告錯誤的等級

如果未設置錯誤處理程序,則 PHP 將根據 php.ini 配置文件處理發生的任何錯誤。error_reporting 指令控制報告和忽略哪些錯誤。雖然也可以在運行時通過調用 error_reporting() 函數來控制,但強烈建議設置配置指令,因為在腳本開始執行之前也可能會發生一些錯誤。

在開發環境中,為了了解並解決 PHP 引發的問題,最好將 error_reporting 設置為 E_ALL 來記錄所有的錯誤。生產環境中,可以將 error_reporting 設置為 E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED 來避免記錄過多信息,但是在多數情況因為下 E_ALL 可以提供早期預警,記錄潛在的問題,也可以用於生產環境。

顯示錯誤或記錄日志

發生錯誤時,PHP 可以采取兩種措施,由另外兩個 php.ini 指令設置:

  • display_errors:輸出錯誤。生產環境中必須禁用,因為它可能包含機密信息(如數據庫密碼),但可用於開發環境,確保立即報告問題。
  • log_errors:記錄錯誤日志。這會將任何錯誤記錄到 error_log 定義的文件或 syslog 中。這在生產環境中非常有用,可以記錄發生的錯誤,然后根據這些錯誤生成報告。

用戶自定義錯誤處理程序

如果 PHP 的默認錯誤處理不滿足需求,還可以使用 set_error_handler() 安裝自己的自定義錯誤處理程序來處理許多類型的錯誤。

一般用於處理用戶通過 trigger_error 觸發的錯誤,大部分 PHP 內置錯誤類型無法以這種方式處理。可以按照腳本認為合適的方式處理那些可以處理的錯誤類型:例如,向用戶顯示自定義錯誤頁面,然后直接發送電子郵件報告錯誤,而不是通過日志。

set_error_handler('myErrorHandler');

function myErrorHandler($severity, $message, $filepath, $line) {
    echo "錯誤信息:".$message;
    // 發送電子郵件...
    exit(1); // 必要時手動終止腳本
}

function myDiv($a, $b) {
    return $a/$b;
}

myDiv(1, 0);
eval('ech 66'); // 無法用自定義的錯誤處理程序

將 Error 變為 ErrorException:

set_error_handler('myErrorHandler');
set_exception_handler('myExceptionHandler');

function myExceptionHandler($exception) {
    echo $exception->getMessage();
}
function myErrorHandler($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) {
        // This error code is not included in error_reporting, so let it fall
        // through to the standard PHP error handler
        return false;
    }
    throw new ErrorException($message, 0, $severity, $file, $line);
}

function myDiv($a, $b) {
    return $a/$b;
}

myDiv(1, 0);

具體的 Error 類

ArithmeticError 算術錯誤

兩種可能的原因:

  • 使用負數移位
  • 調用 intdiv() 方法時,分子是 PHP_INT_MIN 且分母為 -1(此時將返回浮點數)。
try {
    $value = 1 << -1;
    intdiv(PHP_INT_MIN, -1);
} catch (ArithmeticError $e) {
    echo $e->getMessage(), "\n";
}

DivisionByZeroError

兩種可能的原因:

  • 模數(%)運算時,分母為 0。
  • 調用 intdiv() 方法時,分母為 0。

注意,在除法(/)運算符中使用零做分母僅發出警告。

try {
    echo 1/0; // 僅警告
    intdiv(1, 0);
    echo 1%0;
} catch (DivisionByZeroError $e) {
    echo $e->getMessage();
}

AssertionError

使用 assert() 語言結構進行斷言時,可能拋出這個錯誤:

ini_set('zend.assertions', 1); // 執行代碼
ini_set('assert.exception', 1); // 允許拋異常

$test = 1;
assert($test === 0);

ParseError

  • 通過 included 或 required 引入文件有語法錯誤
  • eval() 解析的字符串有語法錯誤
try {
    eval('ech 66');
    include 'has-error.php';
} catch (ParseError $e) {
    echo $e->getMessage(), "\n";
}
syntax error, unexpected '66' (T_LNUMBER)

TypeError

函數的參數或返回值跟類型不匹配時,拋 TypeError:

function add(int $left, int $right) {
    return $left + $right;
}

try {
    $value = add('left', 'right');
} catch (TypeError $e) {
    echo $e->getMessage(), "\n";
}
Argument 1 passed to add() must be of the type integer, string given, called in D:\workspace\szhz\application\controllers\tuan\Index.php on line 312

Exception

Exception 出現的原因

PHP 在使用異常機制之前,通過返回錯誤碼來表示函數的執行結果。部分函數返回 TRUE 或 FALSE,部分函數返回 0 或 1、-1。難以統一且無法包含足夠的報錯原因等信息。例如 strtotime() 函數,成功則返回時間戳,否則返回 FALSE,但是在 PHP 5.1.0 之前本函數在失敗時返回 -1。

異常機制避免了錯誤碼機制的一些不足,可以在 一次捕獲多個異常。異常對象包含錯誤信息、錯誤碼、錯誤行號、文件、上下文,更方便定位問題。

Exception 特點

Exception 是必須手動拋出並且可被捕獲的。如果拋出的異常未被捕獲,則導致 Fatal error,並使得代碼停止執行。

function myDiv($a, $b) {
    if ($b == 0) 
        throw new Exception('Divided by zero');

    return $a/$b;
}

try {
    myDiv(1, 0); // 如果不捕獲異常,則報錯 Fatal error,並停止執行
} catch (Exception $e) {
    echo $e->getMessage(), "\n";
}

// 異常捕獲后,可以繼續執行后面的代碼
...

自定義 Exception

自定義的 Exception 需要繼承自已有異常,定義完成后就可以在代碼中拋出自定義的這些異常。

<?php class pdoDbException extends PDOException { public function __construct(PDOException $e) { if(strstr($e->getMessage(), 'SQLSTATE[')) { echo 'this is my exception'; } } } function f() { try { $pdo = new PDO('123.207.7.188', '$username', '$password', []); } catch (PDOException $e) { throw new pdoDbException($e); } } try { f(); } catch (pdoDbException $e) { print_r($e); }

用戶自定義異常處理程序

set_exception_handler('myExceptionHandler');

function myExceptionHandler($exception) {
    echo $exception->getMessage();
}

function myDiv($a, $b) {
    if ($b == 0)
        throw new Exception('Divided by zero');

    return $a/$b;
}

myDiv(1, 0);

// 自定義異常處理程序執行后,不會繼續執行后面的代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM