[原創]PHP 異常錯誤處理


錯誤與異常

錯誤異常 在PHP 中不一樣的, 它們都表明代碼出現問題, 且都能提供錯誤信息.

Points:

  • 錯誤 出現的時機比異常早
  • 錯誤 可以委托給全局錯誤處理器處理, 有些錯誤是無法恢復的, 會導致腳本停止
  • 異常 要先實例化(Exception類), 然后拋出, 可以被捕獲(try...catch)
  • 異常 捕獲后可以就地處理, 無需停止腳本(任何未被捕獲的異常都會導致腳本停止)

異常的使用:

  • 主動出擊: 在遇到無法修復的狀況時(當前上下文不知道如何處理)主動拋出, 交由使用者處理

    eg. 數據庫連接超時, 傳入參數類型不符合條件等.

    eg. 組件和框架的作者尤其無法確定如何處理異常狀況, 通常會拋出異常, 交由具體使用者去處理.

  • 被動防守: 預測潛在的問題, 減輕影響(將可能拋出異常的代碼放在 try/catch 塊中)

PHP 7 注意:

PHP 7中, 大多數錯誤被作為 Error異常 拋出, 能夠被捕獲.

若未被捕獲且未注冊異常處理函數(通過 set_exception_handler() 注冊), 則會按照傳統方式處理(指PHP7之前版本): 被報告為一個致命錯誤(Fatal Error), 可被 set_error_handler() 處理.

異常類

PHP 內置異常類:

SPL 擴充的異常類(均繼承自 Exception 類):

錯誤類(PHP >= 7)

  • Throwable
    • Error
      • ArithmeticError
        • DivisionByZeroError
      • AssertionError
      • ParseError
      • TypeError
    • Exception
      • ...

注意: PHP 7 中, Error 和 Exception 都繼承自 Throwable, 因此在捕獲(try...catch)時可通過捕獲 Throwable 來同時捕獲異常和錯誤

try {
    // do something
} catch (\Throwable $e) {
    // log error or sth.
}

錯誤

php 能觸發不同類型的錯誤:

  • 致命錯誤
  • 運行時錯誤
  • 編譯時錯誤
  • 啟動錯誤
  • 用戶觸發錯誤(少見)

錯誤報告級別

error_reporting(int $level);

PHP 5.3 及以上, 默認的錯誤報告級別是 E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED (不會顯示 E_NOTICEE_STRICTE_DEPRECATED)

PHP Manual 所有的錯誤級別

部分錯誤級別解釋:

錯誤級別 解釋 建議
E_NOTICE 運行時通知。表示腳本遇到可能會表現為錯誤的情況,但是在可以正常運行的腳本里面也可能會有類似的通知。 開發期間使用, 會對代碼中可能出現的bug進行警告, 節省調試時間
E_STRICT 啟用 PHP 對代碼的修改建議,以確保代碼具有最佳的互操作性和向前兼容性。
E_ALL 不包含 E_STRICT, 因此默認不激活
開發期間使用
E_DEPRECATED 運行時通知。啟用后將會對在未來版本中可能無法正常工作的代碼給出警告。 開啟

錯誤報告設置

錯誤報告遵循原則

  • 報告錯誤
  • 開發環境要顯示錯誤
  • 生產環境要不能顯示錯誤(安全考慮)
  • 開發環境和生產環境都要記錄錯誤

注意分清 報告錯誤顯示錯誤 這兩個概念的區別.

php.ini

開發環境推薦錯誤報告方式

;顯示錯誤
display_startup_errors = On
display_errors = On

;報告錯誤
error_reporting = -1

;記錄錯誤
log_errors = On

生產環境推薦錯誤報告方式

;顯示錯誤
display_startup_errors = Off
display_errors = Off

;報告錯誤
error_reporting = E_ALL & ~E_NOTICE

;記錄錯誤
log_errors = On

部分參數解釋

參數 解釋 建議
display_errors 設置是否將錯誤信息作為輸出的一部分顯示到屏幕,或者對用戶隱藏而不顯示 開發環境打開
生產環境務必關閉
display_startup_errors 即使 display_errors 設置為開啟, PHP 啟動過程中的錯誤信息也不會被顯示。強烈建議除了調試目的以外,將 display_startup_errors 設置為關閉。 開發環境打開
生產環境務必關閉
log_errors 設置是否將腳本運行的錯誤信息記錄到服務器錯誤日志或者error_log之中 打開

范例代碼

<?php

// 關閉所有PHP錯誤報告
error_reporting(0);

// Report simple running errors
error_reporting(E_ERROR | E_WARNING | E_PARSE);

// 報告 E_NOTICE也挺好 (報告未初始化的變量
// 或者捕獲變量名的錯誤拼寫)
error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);

// 除了 E_NOTICE,報告其他所有錯誤
error_reporting(E_ALL ^ E_NOTICE);

// 報告所有 PHP 錯誤 (參見 changelog)
error_reporting(E_ALL);

// 報告所有 PHP 錯誤
error_reporting(-1);

// 和 error_reporting(E_ALL); 一樣
ini_set('error_reporting', E_ALL);

?>

全局異常處理程序

捕獲所有未被捕獲的異常: 通過 set_exception_handler 注冊全局異常處理程序.

函數

set_exception_handler ( callable $exception_handler ) : callable
// 注冊異常處理程序
set_exception_handler('handleException');

// 重置異常處理程序為默認值
// set_exception_handler(null);

// 還原成之前的異常處理程序
// restore_exception_handler();
    
// < PHP 7
handleException(Exception $ex)
{
    // 記錄錯誤日志
    echo "Uncaught exception: " , $ex->getMessage(), "\n";
    // 開發環境顯示調試信息(推薦 filp/whoops 擴展包)
    // ...
    // 生產環境顯示對用戶友好的頁面(信息)
    // ...
    
}

// >= PHP 7
// 大多數錯誤拋出 Error 異常, 也能被捕獲, 因此參數類型必須是 Throwable, 否則會引起問題.
handleException(Throwable $ex)
{
    // ...
}

在用戶自定義異常處理函數內部, 可根據情況做一下處理:

  • 日志記錄錯誤
  • web 渲染錯誤頁面
  • console 渲染錯誤提示

全局錯誤處理函數

通過設置全局錯誤處理程序, 使用自己的自定義方式攔截並處理PHP錯誤, 包括但不限於:

  • 記錄詳細錯誤日志
  • 對數據/文件做清理回收
  • 轉換成 ErrorException 對象, 再由處理異常的流程來處理錯誤.

注冊全局錯誤處理程序

set_error_handler( callable $error_handler [, int $error_types = E_ALL | E_STRICT ] ) : mixed

$error_types 指定的錯誤類型會被該錯誤處理函數攔截 ( 除非該函數返回了 false),不受 error_report() 影響.

處理程序

# 錯誤處理函數參數
# $errno 錯誤等級(對應 E_* 常量)
# $errstr 錯誤消息
# $errfile 發生錯誤的文件名
# $errline 發生錯誤的行號
# $errcontext 一個數組, 指向錯誤發生時可用的符號表(可選參數), 通常不用(php7.2后廢棄)
function error_handler(int $errno, string $errstr, string $errfile, int $errline, array $errcontext) {
	// 處理錯誤
}

帶 @ 前綴的語句發生錯誤時, $errno 值為 0

  • 腳本會在錯誤處理函數結束后從出錯的地方繼續執行 (因此必要時需主動調用 die()exit() 以結束腳本)

  • 如果錯誤發生在腳本執行之前(比如文件上傳時),將不會 調用自定義的錯誤處理程序因為它尚未在那時注冊。

  • 如果函數返回 FALSE,標准錯誤處理處理程序將會繼續調用。

無法捕獲的錯誤類型

以下級別的錯誤不能由用戶定義的錯誤處理函數來捕獲:

錯誤級別 解釋
E_ERROR 致命的運行錯誤, 一般不可恢復(eg. 內存分配導致的問題)
E_PARSE 編譯時語法解析錯誤, 由分析器產生
E_CORE_ERROR PHP初始化啟動過程中發生的致命錯誤, 由php引擎核心產生
E_CORE_WARNING PHP初始化啟動過程中發生的警告(非致命錯誤), 由php引擎核心產生
E_COMPILE_ERROR 致命編譯時錯誤。類似E_ERROR, 但是是由Zend腳本引擎產生的。
E_COMPILE_WARNING 編譯時警告 (非致命錯誤)。類似 E_WARNING,但是是由Zend腳本引擎產生的。

以及在 調用 set_error_handler() 函數所在文件中產生的大多數 E_STRICT

這些無法捕獲的錯誤, 可在 register_shutdown_function() 中處理( 但腳本仍會結束 )

范例代碼

function handleError($errno, $errstr, $errfile = '', $errline = 0)
{
    if (!(error_reporting() & $errno)) {
        // 錯誤類型未包含在 error_reporting() 里, 因此將它交由PHP標准錯誤處理程序來處理
    	return false;
    }
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}

錯誤 轉換為 異常

開發/生產環境處理錯誤和異常

開發環境

php 默認的錯誤信息很糟糕, 為了更高的幫助調試程序, 可以使用 filp/whoops 擴展包.

安裝

composer require filp/whoops

使用(web)

$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

如果php腳本觸發PHP錯誤, 或應用沒有捕獲異常, 則開發人員就可以看到 Whoops 的圖形化診斷頁面.

可用的處理器

生產環境

記錄錯誤信息通常使用 error_log() 函數以將錯誤信息記錄到文件系統或syslog.

error_log ( string $message [, int $message_type = 0 [, string $destination [, string $extra_headers ]]] ) : bool

message_type 參數, 設置錯誤應該發送到何處。可能的信息類型有以下幾個:

0 message 發送到 PHP 的系統日志,使用 操作系統的日志機制或者一個文件,取決於 error_log 指令設置了什么。 這是個默認的選項。
1 message 發送到參數 destination 設置的郵件地址。 第四個參數 extra_headers 只有在這個類型里才會被用到。
2 不再是一個選項。
3 message 被發送到位置為 destination 的文件里。 字符 message 不會默認被當做新的一行。
4 message 直接發送到 SAPI 的日志處理程序中。

一個更好的選擇是使用 monolog/monolog 擴展包

<?php
require "vendor/autoload.php";

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

// create a log channel
$log = new Logger('name');
$log->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));

// add records to the log
$log->warning('Foo');
$log->error('Bar');

示例: 在生產環境中使用 Monolog 記錄日志, 嚴重錯誤使用郵件通知

依賴: swiftmailer/swiftmailer

require "vendor/autoload.php";

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SwiftMailerHandler;

date_default_timezone_set('Asia/Shanghai');

// 設置monolog
$logger = new Logger('my-app-name');
$logger->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));

// 添加SwiftMailer Handler, 遇到嚴重錯誤時使用郵件通知
// Create the Transport
$transport = (new Swift_SmtpTransport('smtp.example.org', 25))
  ->setUsername('your username')
  ->setPassword('your password');

// Create the Mailer using your created Transport
$mailer = new Swift_Mailer($transport);

// Create a message
$message = (new Swift_Message('Wonderful Subject'))
  ->setFrom(['john@doe.com' => 'John Doe'])
  ->setTo(['receiver@domain.org', 'other@domain.org' => 'A name']);

$logger->pushHandler(new SwiftMailerHandler($mailer, $message, Logger::CRITICAL));

// 使用日志記錄器
$logger->critical('The server is on fire!');

php中止時的回調函數

register_shutdown_function

register_shutdown_function(function () {
    // do sth...
}, $para1, $param2, ...)

注冊一個 callback ,它會在腳本執行完成或者 exit() 后被調用。

Note:

  • 可注冊多個回調函數(不會互相覆蓋, 依照注冊順序依次調用), 在php腳本中止時會被調用到.

  • 如果在注冊的方法內部調用 exit(), 那么所有處理會被中止,並且其他注冊的中止回調也不會再被調用。

由於部分錯誤無法被 set_error_handler 捕獲, 因此需配合 register_shutdown_function, 判斷腳本退出的原因, 若是因為未被捕獲的致命錯誤, 則需要處理(日志記錄等)

register_shutdown_function('handleShutdown');

function handleShutdown()
{
    // 如果是因為嚴重錯誤(未被捕獲)導致腳本退出, 則需要處理(作為對 set_error_handler的補充)
	if (! is_null($error = error_get_last()) && isFatal($error['type'])) {
        // handleException() 函數同時處理 set_exception_handler
        handleException(new \ErrorException(
        	$error['message'], $error['type'], 0, $error['file'], $error['line'],
        ));
    }
}

function isFatal($type)
{
    // 以下錯誤無法被 set_error_handler 捕獲: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING
    return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]);
}

Note:

進程被信號 SIGTERM 或 SIGKILL 殺死時中止函數不會被調用. 可通過 pcntl_signal 捕獲信號, 再在其中調用 exit() 來進行正常中止.


免責聲明!

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



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