PHP 錯誤處理最佳實踐:打造易於維護的程式碼
你剛部署了一個新功能,突然你的 PHP 應用程式拋出致命錯誤。使用者看到空白頁面,而你的日誌卻空空如也。聽起來很熟悉嗎?糟糕的錯誤處理是 PHP 程式碼變得難以維護的最常見原因之一。在本文中,我們將逐步探討實用的 PHP 錯誤處理最佳實踐,讓你的程式碼更穩健、更容易除錯,也更易於維護。
為什麼 PHP 錯誤處理很重要
PHP 預設很寬容:它經常在警告之後繼續執行,而錯誤可能被無聲地忽略。這種靈活性是一把雙面刃。如果沒有適當的處理,錯誤可能會:
- 向使用者暴露敏感資訊(例如,堆疊追蹤中的資料庫憑證)。
- 損壞資料或讓應用程式處於不一致的狀態。
- 讓除錯變成一場噩夢,因為錯誤四散各處或被抑制。
良好的錯誤處理能確保當問題發生時,你知道它、能快速修復它,而你的使用者也能獲得優雅的體驗。
1. 設定適當的錯誤報告層級
第一步是為你的環境正確設定 PHP 的錯誤報告。在開發環境中,你會想看到所有錯誤;在生產環境中,你會想記錄它們但不顯示出來。
// 開發環境
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// 生產環境
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/path/to/php-error.log');
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
使用環境變數或設定檔來自動切換這些設定。絕不要依賴手動編輯生產伺服器上的 php.ini。
2. 使用例外而非錯誤碼
回傳錯誤碼(例如 false 或 -1)是一種遺留模式,會讓你的程式碼變得雜亂,而且很容易忽略失敗。例外會強制你明確處理錯誤,並讓你的正常路徑保持乾淨。
// 不好:錯誤碼
function getUser($id) {
$user = db_find($id);
if (!$user) {
return false; // 呼叫者必須檢查
}
return $user;
}
// 好:例外
function getUser($id) {
$user = db_find($id);
if (!$user) {
throw new UserNotFoundException("User $id not found");
}
return $user;
}
為不同的錯誤類型建立自訂例外類別。這讓你能輕鬆捕捉特定錯誤並適當地處理它們。
3. 在正確的層級捕捉例外
一個常見的錯誤是過早或過於廣泛地捕捉例外。只有當你真正能對例外做些什麼時才捕捉它們——記錄、重試,或顯示使用者友善的訊息。
try {
$user = getUser($id);
$order = createOrder($user, $items);
} catch (UserNotFoundException $e) {
// 特別處理找不到使用者的情況
return response('User not found', 404);
} catch (PaymentFailedException $e) {
// 處理付款失敗
return response('Payment failed: ' . $e->getMessage(), 400);
} catch (Throwable $e) {
// 捕捉所有未預期的錯誤
log_error($e);
return response('Something went wrong', 500);
}
使用 Throwable(PHP 7+)來同時捕捉例外和錯誤。避免空的 catch 區塊——如果你捕捉了,就要做點有意義的事。
4. 記錄帶有上下文的錯誤
日誌記錄是你除錯生產問題的最佳夥伴。但像「Error occurred」這樣的日誌訊息毫無用處。要包含上下文:使用者 ID、請求參數、堆疊追蹤和時間戳記。
try {
processPayment($order);
} catch (PaymentException $e) {
error_log(sprintf(
"Payment failed for order %d: %s in %s:%d\nStack trace: %s",
$order->id,
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
));
throw $e; // 記錄後重新拋出
}
考慮使用像 Monolog 這樣的日誌函式庫來產生結構化日誌。它支援不同的處理常式(檔案、syslog、Slack)和日誌層級(debug、info、warning、error)。
5. 建立自訂錯誤處理常式
PHP 的預設錯誤處理常式會將錯誤印到螢幕上,這不適合生產環境。自訂錯誤處理常式讓你將錯誤轉換為例外、記錄它們,或顯示友善的錯誤頁面。
set_error_handler(function ($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) {
return false; // 尊重 error_reporting 設定
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
set_exception_handler(function ($e) {
log_error($e);
http_response_code(500);
include 'views/error.php';
});
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
log_error(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']));
}
});
這個設定確保所有錯誤——包括致命錯誤——都會被記錄並優雅地處理。
6. 驗證輸入並及早失敗
許多錯誤源於無效的輸入。在應用程式的邊界(控制器、API 端點)驗證資料,並在驗證失敗時立即拋出例外。這能防止錯誤深入傳播到你的程式碼中。
function createUser(array $data) {
if (empty($data['email'])) {
throw new InvalidArgumentException('Email is required');
}
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
// ... 自信地繼續
}
使用 PHP 的過濾函式或驗證函式庫(例如 Respect\Validation)來保持驗證的一致性。
7. 不要用 @ 抑制錯誤
@ 運算子會讓錯誤靜音,使除錯更加困難。在呼叫可能發出警告的函式(例如 file_get_contents)時,使用它很誘人,但它會隱藏真正的問題。相反地,檢查前置條件或使用帶有例外的 try-catch。
// 不好
$content = @file_get_contents($url);
// 好
if (!is_readable($url)) {
throw new RuntimeException("Cannot read $url");
}
$content = file_get_contents($url);
如果你必須抑制,只在完全理解的情況下這樣做,並記錄原因。
8. 使用集中式錯誤處理中介軟體
在 Laravel 或 Symfony 這類框架中,錯誤處理通常集中在中介軟體或例外處理常式中。如果你正在建構自己的框架,建立一個單一入口點來捕捉所有例外並將它們轉換為 HTTP 回應。
// 在你的前端控制器(index.php)中
try {
$response = $router->dispatch($request);
} catch (HttpException $e) {
$response = new Response($e->getMessage(), $e->getStatusCode());
} catch (Throwable $e) {
log_error($e);
$response = new Response('Internal Server Error', 500);
}
$response->send();
這會將錯誤處理邏輯集中在一個地方,並確保一致性。
比較:錯誤處理方法
| 方法 | 優點 | 缺點 |
|---|---|---|
| 錯誤碼 | 簡單,不使用例外 | 容易被忽略,使程式碼雜亂 |
| 例外 | 強制處理,分離乾淨 | 可能被過度使用,有效能負擔 |
| 自訂錯誤處理常式 | 集中化,捕捉所有錯誤 | 需要設定,若配置錯誤可能掩蓋錯誤 |
| 僅日誌記錄 | 非侵入性,適合監控 | 不處理錯誤,只記錄 |
常見問題
PHP 中錯誤和例外有什麼區別?
錯誤是低階問題,例如語法錯誤或類型錯誤,而例外是代表異常狀況的拋出物件。在 PHP 7+ 中,兩者都實作了 Throwable 介面,因此你可以用單一 catch 區塊同時捕捉兩者。
我應該對每個函式呼叫都使用 try-catch 嗎?
不,那會導致過度防禦性的程式碼。只有當你能有意義地處理例外時才捕捉它們——記錄、重試,或顯示使用者友善的訊息。讓例外向上傳遞到中央處理常式來處理未預期的情況。
如何在不暴露敏感資料的情況下記錄錯誤?
透過移除密碼、權杖和個人資料來清理日誌訊息。使用帶有上下文字段的結構化日誌記錄,並設定你的日誌函式庫來遮蔽敏感鍵。此外,確保日誌檔案安全儲存並限制存取。
準備好簡化你的 PHP 錯誤處理了嗎?從審查你目前的錯誤報告設定並實作自訂錯誤處理常式開始。若要快速除錯 JSON 酬載或日誌,試試我們的 JSON Formatter 來驗證和美觀列印你的資料。