搜尋
首頁後端開發PHP7討論php的錯誤和異常處理機制

討論php的錯誤和異常處理機制

「宣告: 本文採用 CC BY-NC-ND 4.0 授權。

原先的 PHP 只有錯誤沒有例外。看一些舊的文檔你能看到不少錯誤輸出是直接 echo html 標籤的。而現代一點的框架早已經包裝好了一切,直接拋出異常就可以有比較漂亮的錯誤顯示頁面,例如 rails 的 better errors。當然,PHP 的現代框架也已經做的不錯了,例如 laravel。然而我司目前還是用 codeigniter 2,它的錯誤和異常處理還比較簡陋。藉著升級到        PHP7 的契機梳理了一下 PHP 的錯誤和異常處理的機制。

推薦教學:《PHP教學

PHP 的錯誤與例外

PHP5 已經實作了例外的處理,這和其他語言差異不大,無非是try, catch, uncaught,按下不表,先說錯誤。

PHP 的錯誤

除了例外 PHP5 常見的就是拋出錯誤。你可以在官方文件找到所有的錯誤的定義,這些錯誤可以大致分為 WARNING, ERROR(fatal error), NOTICE 等1。 PHP的錯誤機制總結一文中給出了每種錯誤出現的場景。

E_DEPRECATED(8192) 執行階段通知,啟用後將會對在未來版本中可能無法正常運作的程式碼給予警告。

E_USER_DEPRECATED(16384) 是由使用者自己在程式碼中使用PHP函數 trigger_error() 來產生的

E_NOTICE(8) 執行時間通知。表示腳本遇到可能會表現為錯誤的情況

E_USER_NOTICE(1024) 是使用者自己在程式碼中使用PHP的trigger_error() 函數來產生的通知訊息

#E_WARNING(2) 執行時期警告(非致命錯誤)

E_USER_WARNING(512) 使用者自己在程式碼中使用PHP的trigger_error() 函數來產生的

#E_CORE_WARNING(32) PHP初始化啟動過程中由PHP引擎核心產生的警告

E_COMPILE_WARNING(128) Zend腳本引擎產生編譯時警告

E_ERROR(1) 致命的執行階段錯誤

E_USER_ERROR(256) 使用者自己在程式碼中使用PHP的trigger_error()函數來產生的

E_CORE_ERROR(16) 在PHP初始化啟動過程中由PHP引擎核心產生的致命錯誤

#E_COMPILE_ERROR(64) Zend腳本引擎產生的致命編譯時錯誤

E_PARSE(4) 編譯時語法解析錯誤。解析錯誤僅由分析器產生

E_STRICT(2048) 啟用PHP 對程式碼的修改建議,以確保程式碼具有最佳的互通性和向前相容性

#E_RECOVERABLE_ERROR(4096) 可被捕捉的致命錯誤。它表示發生了一個可能非常危險的錯誤,但還沒有導致PHP引擎處於不穩定的狀態。如果該錯誤沒有被使用者自訂句柄捕獲 (參見 set_error_handler() ),將成為一個 E_ERROR  從而腳本會終止執行。

E_ALL(30719) 所有錯誤和警告訊息(手冊上說不包含E_STRICT, 經過測試其實是包含E_STRICT的)。

常見的有:

<?php // E_ERROR
nonexist(); // PHP Fatal error:  Call to undefined function nonexist()
throw new Exception(&#39;&#39;); // 未捕获异常也是 fatal error

// E_NOTICE
$a = $b; //  PHP Notice:  Undefined variable
$a = []; $a[2]; // PHP Notice:  Undefined offset: 2

// E_WARNNING
require &#39;nonexist.php&#39; // warning and fatal error
   

由於歷史原因,這個老舊的 ci2 框架有不少不合理的地方,例如會讀取不存在的 log 文件;我們對 PHP 也有一些不規範的使用,例如:

<?php $req = [];
$user_id = $req[&#39;user_id&#39;]; // PHP error:  Undefined offset
if (null === $user_id) { /* do something */}
   

我們的程式碼不少地方較為依賴這種獲取不存在 key 得到 null 的表現,而每次這樣使用都是會有一個 E_NOTICE 錯誤的。雖然可以透過 array_exists 來做 if else,但畢竟比較麻煩。 PHP7 之後可以透過資料結構插件來使用 Map, Set, Vector 等明確的資料結構,從而較好的解決這個問題。

PHP 對錯誤的處理

如果沒有做任何配置,PHP 的錯誤是會直接印出來的。古老的 PHP 應用程式也確實有這麼做的。但現代應用顯然不能這樣,現代應用的錯誤應該遵循一下規則2

#一定要讓PHP 報告錯誤;

在開發環境中要顯示錯誤;

在生產環境中不能顯示錯誤;

在開發和生產環境中都要記錄錯誤。

在生產環境下,錯誤不能直接列印出來,應該記到 log 檔案中,並傳回使用者一個籠統的錯誤訊息。 set_error_handler 函數就是設定使用者自訂的錯誤處理函數,以處理腳本中出現的錯誤。我們可以在這個函數中將錯誤訊息打到 log 檔案中,並統一回傳錯誤訊息。

本来这个函数是搭配 trigger_error 函数使用的。用户通过 trigger_error 产生 error,然后用 error_handler 来处理错误。只是在这种场景下往往「异常」更好用,所以这么用的并不多。

在前述的系统自带的 16 种错误中,有一部分相当重要的错误并不能被 error_handler 捕获3

以下级别的错误不能由用户定义的函数来处理: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、E_COMPILE_WARNING,和在调用 set_error_handler() 函数所在文件中产生的大多数 E_STRICT。

这些错误将无法记录下来,同时也不方便统一处理4。在 PHP7 之前的 PHP 版本一个很大的痛点就是:发生了 E_ERROR 错误,无法捕获,导致数据库的事务无法回滚造成数据不一致5

另外一个需要注意的是, error_handler 处理完毕,脚本将会继续执行发生错误的后一行。在某些情况下,你可能希望遇到某些错误可以中断脚本的执行。在官方文档中已说明,

同时注意,在需要时你有责任使用 die()。 如果错误处理程序返回了,脚本将会继续执行发生错误的后一行。

也就是说,某些情况下,我们处理完 E_WARNING 之后,需要及时退出脚本(即 die() 或者 exit())。

PHP 异常

异常是对程序错误的一种优秀的处理方式,较于错误,异常的优点是默认打印调用栈,便于调试,可控等,可以参考一下鸟哥的文章我们什么时候应该使用异常,清晰的点明了错误码和异常的优缺点。

对异常的处理也要遵循前述的错误处理规则2。在我们的日常开发中,不可能保证可以 catch 所有的异常,而未被 catch 的异常将以 fatal error 的形式中断脚本的执行并输出错误信息。所以要借助 set_exception_handler,统一处理所有未被        catch 的异常。我们可以像 error_handler 那样,在 exception_handler 中处理 log,将数据库的事务回滚。

前面提到,error_handler 需要在必要的时候手动中断脚本, PHP 文档中给出的一种实践是,在 error_handler 中 throw ErrorException,代码示例如下:

<?php function exception_error_handler($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) {
        // This error code is not included in error_reporting
        return;
    }
    throw new ErrorException($message, 0, $severity, $file, $line);
}
set_error_handler("exception_error_handler");

/* Trigger exception */
strpos();
   

这样凡是不想忽略的 error,都会以 Uncaught ErrorException 的形式返回并中断脚本。

PHP 异常机制

鸟哥通过一个例子讲解了 PHP 的异常的处理机制,在这里转述一下。

<?php function onError($errCode, $errMesg, $errFile, $errLine) {
    echo "Error Occurred\n";
    throw new Exception($errMesg);
}
 
function onException($e) {
    echo &#39;********exception: &#39; . $e->getMessage();
}
 
set_error_handler("onError");
 
set_exception_handler("onException");

require("nonexist.php");
   

其运行结果为

  1. Error Occurred
  2. PHP Fatal error

而 onException 并没有执行到,说明在 error_handler 中 throw exception 不会被 exception_handler 截获。

require 不存在的文件会抛出两个错误,

  1. WARNING : 在PHP试图打开这个文件的时候抛出
  2. E_COMPILE_ERROR : 从PHP打开文件的函数返回失败以后抛出

PHP 中的异常处理机制如下:

   

而PHP在遇到 Fatal Error 的时候,会直接 zend_bailout,而 zend_bailout 会导致程序流程直接跳过上面代码段,也可以理解为直接 exit 了(longjmp),这就导致了 user_exception_handler 没有机会发生作用。

PHP 错误分类

综上所述,在 PHP 中,错误和异常可以分为以下 3 个类别:异常,可截获错误,不可截获错误。异常和可截获错误虽然机理不同,但可以当做是同一种处理方式,而不可截获错误是另一种,是一种较为棘手的错误类型。马上将会讲到,PHP7 中的 fatal error 是一种继承自 Throwable 的 Error,是可以被 try catch 住的。通过这一方式 PHP7 解决了这一难题。

PHP7 的错误和异常

PHP 7 改变了大多数错误的报告方式。不同于传统(PHP 5)的错误报告机制,现在大多数错误被作为 Error 异常抛出(在 PHP7 中,只有 fatal error 和 recoverable error 抛出异常,其他 error 比如 warning 和 notice 的表现不变6)。PHP7 中的 Error 和 Exception 的关系如图        6

interface Throwable
    |- Exception implements Throwable
        |- ...
    |- Error implements Throwable
        |- TypeError extends Error
        |- ParseError extends Error
        |- ArithmeticError extends Error
            |- pisionByZeroError extends ArithmeticError
        |- AssertionError extends Error
   

值得注意的是,Error 类表现上和 Exception 基本一致,可以像 Exception 异常一样被第一个匹配的 try / catch 块所捕获,如果没有匹配的 catch 块,则调用异常处理函数(事先通过        set_exception_handler() 注册7)进行处理。 如果尚未注册异常处理函数,则按照传统方式处理,被报告为一个致命错误(Fatal Error)。但并非继承自 Exception 类(要考虑到和 PHP5 的兼容性),所以不能用 catch (Exception        $e) { ... } 来捕获,而需要使用 catch (Error $e) { ... },当然,也可以使用 set_exception_handler 来捕获。

但是,用户不能自己定义类实现 Throwable,这是为了保证只有 Exception 和 Error 才可以抛出。

PHP7 的 ERROR 处理

PHP7 中的 fatal error 会抛出 Error,且可以被正常 catch 到:

<?php $a = 1;
try {
  $a->nonexist();
} catch (Error $e) {
  // Handle error
}
   

也有些错误场景下会抛出更加详细的错误,比如:

<?php // TypeError
function test(int $i) {
  echo $i;
}
try {
  test(&#39;test&#39;);
} catch (TypeError $e) {
  // Handle error
}

// ParseError
try{
  eval(&#39;i=1;&#39;);
} catch (ParseError $e) { 
  echo $e->getMessage(), "\n";
}

// ArithmeticError
try {
    $value = 1 getMessage(), "\n";
}

// pisionByZeroError
try {
    $value = 1 % 0;
} catch (pisionByZeroError $e) {
    echo $e->getMessage(), "\n";
}
   

Error 和 Exception 的选择

当需要自定义处理错误的时候,应该选择继承 Error 还是 Exception 呢?

我们注意到,PHP7 中是将曾经的 fatal error 变成了 Error 抛出,而 fatal error 一般都是一些不需要在运行时处理的错误,这种错误旨在提醒程序员,这里的代码写的有问题,需要修复,而不是逻辑上要 catch 它做某些业务。

因此,绝大多数情况下,我们并不需要继承 Error,甚至 catch Error 也不常见,只在某些需要 log,回滚数据库,清理现场等场合才需要这样做。

对错误和异常的一种实践

根据以上所述,我们提炼了一个对错误和异常处理较好的实践。

  1. 对于业务中不应该出现错误的地方,抛出 InternalException,而不是 Error
<?php class InternalException extends Exception { /*...*/ }

function find(Array $ids) {
  if (empty($ids)) {
    throw new InternalException(&#39;ids should not be empty&#39;);
  }
  ...
}
   
  1. 只在需要清理现场的时候 catch Error
<?php try { /*...*/ }
catch (Throwable $t) {
  // log, transaction rollback, cleanup...
}
   
  1. 未捕获的 Error 和 Exception 通过 set_exception_handler 做后续清理和 log
  2. 其他错误仍然通过 set_error_handler 来处理,在处理的时候使用更加明确的 FriendlyErrorType,并抛出 ErrorException 记录调用栈

FriendlyErrorType:

<?php function FriendlyErrorType($type) 
{ 
    switch($type) 
    { 
        case E_ERROR: // 1 // 
            return &#39;E_ERROR&#39;; 
        case E_WARNING: // 2 // 
            return &#39;E_WARNING&#39;; 
        case E_PARSE: // 4 // 
            return &#39;E_PARSE&#39;; 
        case E_NOTICE: // 8 // 
            return &#39;E_NOTICE&#39;; 
        case E_CORE_ERROR: // 16 // 
            return &#39;E_CORE_ERROR&#39;; 
        case E_CORE_WARNING: // 32 // 
            return &#39;E_CORE_WARNING&#39;; 
        case E_COMPILE_ERROR: // 64 // 
            return &#39;E_COMPILE_ERROR&#39;; 
        case E_COMPILE_WARNING: // 128 // 
            return &#39;E_COMPILE_WARNING&#39;; 
        case E_USER_ERROR: // 256 // 
            return &#39;E_USER_ERROR&#39;; 
        case E_USER_WARNING: // 512 // 
            return &#39;E_USER_WARNING&#39;; 
        case E_USER_NOTICE: // 1024 // 
            return &#39;E_USER_NOTICE&#39;; 
        case E_STRICT: // 2048 // 
            return &#39;E_STRICT&#39;; 
        case E_RECOVERABLE_ERROR: // 4096 // 
            return &#39;E_RECOVERABLE_ERROR&#39;; 
        case E_DEPRECATED: // 8192 // 
            return &#39;E_DEPRECATED&#39;; 
        case E_USER_DEPRECATED: // 16384 // 
            return &#39;E_USER_DEPRECATED&#39;; 
    } 
    return ""; 
}
   

error_handler:

<?php function exception_error_handler($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) {
        // This error code is not included in error_reporting
        return;
    }
 	log FriendlyErrorType($severity);
    throw new ErrorException($message, 0, $severity, $file, $line);
}
set_error_handler("exception_error_handler");
   
  1. PHP中的错误级别与具体报错信息分类 ↩

  2. PHP 最佳实践之异常和错误 ↩ ↩2

  3. E_ERROR 无法捕获,E_RECOVERABLE_ERROR 可以,后者默认输出 Catachable fatal error ↩

  4. fatal error 会记录到 web 服务器的 error.log,这一点需要注意,因为这个 log 的位置往往不是 PHP 应用定义的,而是 web 服务器定义的。 ↩

  5. PHP 中还有一个 register_shutdown_function 函数,它允许注册一个会在 PHP 中止时执行的函数,这个函数可以捕获 fatal error,毕竟是只要是脚本中断就可以捕获的。ci2 并没有使用这个方法,所以相关问题一直没有得到很好的解决,当时也没有意识到这个函数的存在,升级 PHP7 之后可以通过                catch Error 来解决,便不再需要这样处理了。 ↩

  6. Throwable Exceptions and Errors in PHP 7 ↩ ↩2

  7. 在 PHP7 中,传入 exception_handler 的参数从 Exception 改为 Throwable,这意味着 exception_handler 可以截获 Error。 ↩

以上是討論php的錯誤和異常處理機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)