調試是將「能跑」變為「穩跑」的關鍵。本文系統梳理 PHP 調試全鏈路:從基礎的錯誤報告配置,到 Xdebug 斷點調試、生產環境日誌分析、性能剖析、業務邏輯驗證、單元測試驅動開發,構建全環境、全生命週期的調試體系。
; php.ini 開發環境
error_reporting = E_ALL | E_STRICT
display_errors = On
display_startup_errors = On
log_errors = On
error_log = /var/log/php_dev.log
html_errors = On
; 可選:Xdebug 增強顯示
xdebug.mode = develop,debug
xdebug.var_display_max_depth = 5
xdebug.var_display_max_children = 256
xdebug.var_display_max_data = 1024
; php.ini 生產環境
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php_prod.log
html_errors = Off
; 關鍵:忽略用戶自定義錯誤處理器的 @ 抑制
; 生產環境嚴禁使用 @ 操作符
<?php
// 開發環境檢測
$isDev = (getenv('APP_ENV') ?? 'prod') === 'dev';
if ($isDev) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
} else {
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php_prod.log');
}
// 自定義錯誤處理器:統一格式、關鍵錯誤告警
set_error_handler(function ($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) return false; // 未在報告範圍內
$log = "[{$severity}] $message in $file:$line\n" . debug_print_backtrace(null, 10, true);
error_log($log);
// 致命錯誤發送告警
if (in_array($severity, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR])) {
// 發送到監控系統:Sentry、DingTalk、Email、Slack
// sendAlert("PHP Fatal Error", $log);
}
return true; // 不執行內建處理器
});
set_exception_handler(function (Throwable $e) {
error_log("[Uncaught Exception] {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}\n{$e->getTraceAsString()}");
// sendAlert("Uncaught Exception", $e->getMessage());
if ($isDev) {
echo "{$e->getMessage()}\n{$e->getTraceAsString()}";
} else {
http_response_code(500);
echo "系統繁忙,請稍後重試";
}
});
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) {
error_log("[Shutdown Fatal] {$error['message']} in {$error['file']}:{$error['line']}");
// sendAlert("Shutdown Fatal", $error['message']);
}
});
# Ubuntu/Debian
apt install php-xdebug
# 或 PECL
pecl install xdebug
# php.ini / xdebug.ini 關鍵配置
zend_extension=xdebug.so
xdebug.mode = debug,develop,trace,profile
xdebug.client_host = 127.0.0.1
xdebug.client_port = 9003
xdebug.start_with_request = trigger # 觸發模式:瀏覽器插件/URL 參數
xdebug.idekey = PHPSTORM
xdebug.log = /var/log/xdebug.log
xdebug.log_level = 7
PHP Debug 擴展,配置 launch.json| 功能 | 用途 | 啟用方式 |
|---|---|---|
| 斷點調試 | 逐行執行、變量檢查、調用棧、表達式求值 | IDE 設置斷點 + 瀏覽器插件觸發 |
| 變量增強顯示 | var_dump 替代品,顏色、摺疊、深度控制 | xdebug.mode=develop 自動生效 |
| 函數調用追蹤 | 生成調用鏈文件,分析流程、發現死循環 | xdebug.mode=trace + xdebug.trace_output_dir |
| 代碼覆蓋率 | 單元測試覆蓋率報告 | xdebug.mode=coverage + PHPUnit --coverage-html |
| 性能剖析 | 生成 cachegrind 文件,KCacheGrind/QCacheGrind 分析 | xdebug.mode=profile + xdebug.profiler_output_dir |
# URL 參數觸發
https://example.com/page?XDEBUG_SESSION_START=PHPSTORM
# Cookie 觸發(瀏覽器插件:Xdebug Helper)
# 設置 IDE Key = PHPSTORM,點擊 Debug 圖標
<?php
// 統一日誌函數
function logInfo(string $message, array $context = []): void {
$record = [
'time' => date('c'),
'level' => 'info',
'message' => $message,
'context' => $context,
'trace_id' => $_SERVER['HTTP_X_TRACE_ID'] ?? uniqid('', true),
'user_id' => $_SESSION['user_id'] ?? null,
'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
'url' => $_SERVER['REQUEST_URI'] ?? null,
];
// 寫入文件(生產建議用 Monolog + 輸出到 stdout,由日誌收集系統接管)
file_put_contents('/var/log/app_info.log', json_encode($record, JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND);
}
function logError(string $message, array $context = [], ?Throwable $e = null): void {
$record = [
'time' => date('c'),
'level' => 'error',
'message' => $message,
'context' => $context,
'exception' => $e ? [
'class' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
] : null,
'trace_id' => $_SERVER['HTTP_X_TRACE_ID'] ?? uniqid('', true),
];
file_put_contents('/var/log/app_error.log', json_encode($record, JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND);
}
# 1. 查看最近 100 條錯誤
tail -100 /var/log/app_error.log | jq .
# 2. 統計錯誤類型分布
jq -r '.exception.class' /var/log/app_error.log | sort | uniq -c | sort -rn
# 3. 追蹤某 Trace ID 全鏈路
grep "abc123trace" /var/log/app_*.log | jq .
# 4. 找出耗時 > 1s 的請求
jq 'select(.context.duration > 1000)' /var/log/app_access.log
# 5. 實時跟蹤(生產調試)
tail -f /var/log/app_error.log | jq .
# 生成 profile 文件
xdebug.mode = profile
xdebug.profiler_output_dir = /tmp/xdebug_profiler
xdebug.profiler_output_name = cachegrind.out.%p
# 分析工具
# Linux: kcachegrind / qcachegrind
# Mac: qcachegrind (brew install qcachegrind)
# Web: https://github.com/jrfnl/Profiler (PHP 解析器)
<?php
// 1. 記憶體使用
echo "Memory: " . memory_get_usage(true) / 1024 / 1024 . " MB\n";
echo "Peak: " . memory_get_peak_usage(true) / 1024 / 1024 . " MB\n";
// 2. 精確計時
$start = hrtime(true);
// ... code ...
$elapsed = (hrtime(true) - $start) / 1e9; // 秒
// 3. 函數調用統計(需 xdebug 或 tideways)
// 4. OPcache 狀態
print_r(opcache_get_status());
<?php
// 1. 臨時輸出(開發環境)
dd($variable); // Laravel helper: dump + die
// 等價:
var_dump($variable); die;
// 2. 條件斷點邏輯
if ($user->id === 12345) {
error_log("DEBUG user 12345: " . json_encode($user->toArray()));
}
// 3. 追蹤變量變化
function trace(string $label, mixed $value): void {
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$caller = $bt[1] ?? [];
error_log("TRACE [$label] {$caller['file']}:{$caller['line']} = " . var_export($value, true));
}
// 使用
trace('before_discount', $order->amount);
$order->applyDiscount($coupon);
trace('after_discount', $order->amount);
<?php
// tests/Service/OrderServiceTest.php
use PHPUnit\Framework\TestCase;
class OrderServiceTest extends TestCase
{
private OrderService $service;
private MockOrderRepository $repo;
protected function setUp(): void {
$this->repo = new MockOrderRepository();
$this->service = new OrderService($this->repo);
}
public function testCalculateTotalWithDiscount(): void {
$order = new Order(items: [
new OrderItem(price: 100, qty: 2),
new OrderItem(price: 50, qty: 1),
]);
$coupon = new Coupon(type: 'percent', value: 10); // 9 折
$total = $this->service->calculateTotal($order, $coupon);
$this->assertEquals(225, $total); // (200+50)*0.9 = 225
}
public function testInsufficientStockThrows(): void {
$this->repo->setStock('SKU001', 0);
$this->expectException(InsufficientStockException::class);
$this->service->placeOrder(['SKU001' => 1]);
}
}
Red(寫失敗測試) → Green(寫最少代碼通過) → Refactor(重構優化) → Repeat
| 場景 | 首選工具 | 備選方案 | 關鍵動作 |
|---|---|---|---|
| 開發環境語法/運行錯誤 | error_reporting + display_errors | Xdebug 斷點 | 開啟全錯誤報告 |
| 變量值不符合預期 | Xdebug 斷點/變量面板 | dd()/var_dump + die | 設置條件斷點 |
| 死循環/邏輯跳轉異常 | Xdebug 調用棧/Trace | xdebug.trace 輸出文件 | 啟用函數追蹤 |
| 頁面空白/500 無報錯 | error_log / nginx error.log | register_shutdown_function | 檢查 display_errors=Off |
| 生產環境異常 | 結構化日誌 + ELK/日誌平台 | Sentry 異常追蹤 | Trace ID 串聯全鏈路 |
| 接口響應慢 | Xdebug Profiler / Blackfire | 慢查詢日誌 + EXPLAIN | 生成分析文件 |
| 內存溢出 | memory_get_peak_usage + Xdebug | 生成器 yield 流式處理 | 分塊處理大數據 |
| 業務邏輯錯誤 | 單元測試復現 + 斷點 | 臨時 trace 輸出 | 編寫失敗測試先行 |
調試不是「修補漏洞」,而是建立可觀測、可驗證、可重構的開發流程。從 error_reporting 起步,引入 Xdebug 斷點調試,建立結構化日誌體系,接入 APM 持續剖析性能,推行 TDD 以測試守護業務正確性。工具鏈成熟後,調試從「盲人摸象」變為「全景透視」,交付質量與效率雙提升。