錯誤與異常
錯誤和異常 在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
- ArithmeticError
- Exception
- ...
- Error
注意: 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_NOTICE、 E_STRICT 、E_DEPRECATED)
部分錯誤級別解釋:
錯誤級別 | 解釋 | 建議 |
---|---|---|
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 的圖形化診斷頁面.
可用的處理器
PrettyPageHandler
- 顯示美觀的web頁面顯示錯誤信息PlainTextHandler
- 用於命令行程序時輸出純文本信息CallbackHandler
- 將閉包或其他可調用包裝為處理程序JsonResponseHandler
- 捕獲異常, 並以 JSON 字符串形式返回, 比如用於 AJAX 請求XmlResponseHandler
- 捕獲異常, 但是是以XML格式字符串返回
生產環境
記錄錯誤信息通常使用 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 記錄日志, 嚴重錯誤使用郵件通知
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(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()
來進行正常中止.