生產環境絕不向用戶顯示詳細錯誤信息,但必須完整記錄到日志供開發排查。通過 php.ini 配置實現「用戶見友好頁面,開發者見詳細日志」。
| 指令 | 開發環境 | 生產環境 | 說明 |
|---|---|---|---|
| display_errors | On | Off | 控制錯誤是否輸出到瀏覽器 |
| display_startup_errors | On | Off | PHP 啟動階段錯誤顯示 |
| error_reporting | E_ALL | E_ALL & ~E_DEPRECATED & ~E_STRICT | 記錄級別(建議生產也用 E_ALL) |
| log_errors | On | On | 啟用錯誤日志記錄 |
| error_log | syslog/文件 | 獨立文件路徑 | 日志文件具體路徑 |
| error_prepend_string | <div class='error'> | 空 | 錯誤前綴(生產禁用) |
| error_append_string | </div> | 空 | 錯誤後綴 |
; ===== 錯誤顯示 =====
display_errors = Off
display_startup_errors = Off
html_errors = Off
; ===== 錯誤報告級別 =====
; 記錄所有錯誤,包含棄用警告(便於升級排查)
error_reporting = E_ALL
; ===== 錯誤日志 =====
log_errors = On
; 絕對路徑,確保 PHP 進程用戶有寫權限
error_log = /var/log/php/error.log
; 日志最大長度(0 = 無限制)
log_errors_max_len = 0
; 忽略重複錯誤(防止日志洪水)
ignore_repeated_errors = On
ignore_repeated_source = On
; 記錄內存泄漏(開發/測試環境建議開啟)
report_memleaks = On
track_errors = Off ; $php_errormsg 已棄用 PHP 7.2
; ===== 異常處理 =====
; 將錯誤轉為異常 (PHP 7+)
; zend.exception_ignore_args = On ; 異常堆棧不包含參數值(安全)
; zend.exception_string_param_max_len = 0 ; 參數字符串長度限制
# 創建日志目錄
mkdir -p /var/log/php
# 設置擁有者為 PHP-FPM 運行用戶 (如 www-data)
chown www-data:www-data /var/log/php
chmod 755 /var/log/php
# 日志輪轉配置 /etc/logrotate.d/php
/var/log/php/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 640 www-data www-data
sharedscripts
postrotate
systemctl reload php7.4-fpm > /dev/null 2>&1 || true
endscript
}
<?php
// bootstrap.php 或入口文件最早引入
function customErrorHandler(int $severity, string $message, string $file, int $line): bool {
// 不在 error_reporting 範圍內的錯誤不處理
if (!(error_reporting() & $severity)) {
return false; // 交給內置處理器
}
// 格式化錯誤信息
$error = [
'time' => date('c'),
'type' => getErrorTypeString($severity),
'message' => $message,
'file' => $file,
'line' => $line,
'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10),
'request' => [
'uri' => $_SERVER['REQUEST_URI'] ?? '',
'method' => $_SERVER['REQUEST_METHOD'] ?? '',
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? ''
]
];
// 寫入日志 (JSON 格式便於 ELK 分析)
error_log(json_encode($error, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
// 生產環境不顯示詳情
if (ini_get('display_errors')) {
echo "" . htmlspecialchars(print_r($error, true)) . "
";
}
return true; // 阻止內置處理器
}
function getErrorTypeString(int $type): string {
$types = [
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_PARSE => 'PARSE',
E_NOTICE => 'NOTICE',
E_CORE_ERROR => 'CORE_ERROR',
E_CORE_WARNING => 'CORE_WARNING',
E_COMPILE_ERROR => 'COMPILE_ERROR',
E_COMPILE_WARNING => 'COMPILE_WARNING',
E_USER_ERROR => 'USER_ERROR',
E_USER_WARNING => 'USER_WARNING',
E_USER_NOTICE => 'USER_NOTICE',
E_STRICT => 'STRICT',
E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
E_DEPRECATED => 'DEPRECATED',
E_USER_DEPRECATED => 'USER_DEPRECATED'
];
return $types[$type] ?? "UNKNOWN($type)";
}
// 註冊處理器
set_error_handler('customErrorHandler');
// 異常處理
set_exception_handler(function(Throwable $e) {
customErrorHandler(E_ERROR, $e->getMessage(), $e->getFile(), $e->getLine());
// 顯示友好錯誤頁面
http_response_code(500);
include __DIR__ . '/templates/500.html';
exit;
});
// 致命錯誤捕獲 (PHP 7+)
register_shutdown_function(function() {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
customErrorHandler($error['type'], $error['message'], $error['file'], $error['line']);
}
});
?>
<!-- templates/500.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>服務器內部錯誤</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
display: flex; align-items: center; justify-content: center;
min-height: 100vh; margin: 0; background: #f5f5f5; }
.card { background: white; padding: 3rem; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; max-width: 400px; }
h1 { color: #e53e3e; margin-bottom: 1rem; }
p { color: #4a5568; line-height: 1.6; }
.ref { font-family: monospace; background: #edf2f7; padding: 0.5rem;
border-radius: 4px; display: inline-block; margin-top: 1rem; }
</style>
</head>
<body>
<div class="card">
<h1>😕 服務器開小差了</h1>
<p>我們已經記錄了這個錯誤,技術團隊正在緊急修復中。</p>
<p>請稍後重試,或聯繫客服獲得幫助。</p>
<div class="ref">錯誤參考: <?= date('YmdHis') ?></div>
</div>
</body>
</html>
#!/bin/bash
# monitor_php_errors.sh
LOG_FILE="/var/log/php/error.log"
ALERT_KEYWORDS="Fatal|Error|Exception|Out of memory|Maximum execution time"
tail -F "$LOG_FILE" | while read line; do
if echo "$line" | grep -qE "$ALERT_KEYWORDS"; then
# 發送告警 (郵件/Slack/鉉釘/企業微信)
curl -X POST "https://webhook.example.com/alert" \
-H "Content-Type: application/json" \
-d "{\"text\":\"PHP 錯誤告警\",\"detail\":\"$line\"}"
fi
done
# 查看最近 100 條錯誤
tail -100 /var/log/php/error.log | jq -r '.time + " " + .type + " " + .message'
# 統計錯誤類型分布
tail -1000 /var/log/php/error.log | jq -r '.type' | sort | uniq -c | sort -rn
# 查找特定文件的錯誤
grep '"file":"\/var\/www\/html\/api\/user.php"' /var/log/php/error.log | jq .
| 框架 | 錯誤處理機制 | 配置建議 |
|---|---|---|
| Laravel | App\Exceptions\Handler + Monolog | APP_DEBUG=false, LOG_CHANNEL=stack |
| Symfony | Kernel::handleException + Monolog | APP_ENV=prod, kernel.debug=false |
| ThinkPHP | think\ExceptionHandle | app_debug = false, log 配置 |
| Yii2 | ErrorHandler + yii\log\Dispatcher | YII_DEBUG=false, YII_ENV=prod |
| CodeIgniter | Exceptions + Log 類 | ENVIRONMENT=production |
框架通常有自己的錯誤處理機制,以框架配置為準,php.ini 作為底層保障。
display_errors = Off 防止信息洩露(路徑、SQL、變量值)log_errors = On 確保可追溯error_log 指向非 Web 可訪問目錄PHP 錯誤處理黃金法則:用戶見友好頁,日志見真章。生產環境關閉 display_errors,開啟 log_errors 指定獨立日志文件,配合自定義錯誤處理器輸出結構化 JSON 日志,接入日志分析平台與告警系統,做到「有錯必錄、有錄必查、有查必改」。