PHP 完善的 Error / Exception 的捕獲與處理

PHP(PHP_VERSION >= 7) 的 Error / Exception 的捕獲與處理仍是值得一說的,優雅處理錯誤與異常,在提高框架友好度的同時,也提高了開發效率。php

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 標準錯誤處理

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');

一、error_reporting([level])

獲取或設定當前錯誤的監聽級別。要注意,是獲取或設定的 PHP 標準錯誤處理 的級別,不會有效於 try...catch... 或 set_error_handler。測試

二、display_errors

是否將錯誤信息回顯至標準輸出。默認開啓,生產環境下強烈建議 關閉 此項。ui

三、log_errors

是否記錄錯誤日誌。默認關閉,生產環境下強烈建議 開啓 此項。編碼

四、error_log

錯誤日誌的保存文件。注意:若是路徑無效,display_errors 會被強制開啓。.net

PHP 用戶自定義錯誤處理

一、set_error_handler

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

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

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?';

四、try ... catch ...

開發中用戶層面的 set_error_hanlder 沒法捕獲的錯誤還剩下 E_ERRORE_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;
}

PHP 預約義的 Error 和 Exception

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,供開發者規範本身的錯誤異常架構。

相關文章
相關標籤/搜索