代码能写出来不算本事,能快速排查出 bug 才是真功夫。排查问题的两大利器是调试工具和日志——前者帮你看清代码运行时的状态,后者帮你还原线上发生的问题。很多新手只会用 var_dump 调试、日志乱写一气,效率低下且上线后两眼一抹黑。本篇讲清建站开发中该用什么调试工具、日志该怎么规范记录。
一、Xdebug:断点调试利器
var_dump 和 dd() 能看变量值,但碰到复杂逻辑、多层调用、循环里的诡异问题就力不从心。Xdebug 让你像调试桌面程序一样打断点——代码执行到断点处暂停,你可以逐行执行、查看所有变量、追踪函数调用栈。一旦用上就再也回不去 var_dump 了。
# 安装 Xdebug(通过 pecl)
pecl install xdebug
# php.ini 配置
[xdebug]
zend_extension=xdebug
xdebug.mode=debug # 开启调试模式
xdebug.start_with_request=yes # 每个请求自动启动调试
xdebug.client_host=127.0.0.1 # IDE 监听地址
xdebug.client_port=9003 # IDE 监听端口
xdebug.log=/tmp/xdebug.log # 调试日志(排查连接问题用)
# VS Code 配置(.vscode/launch.json)
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www/html": "${workspaceFolder}"
}
}
]
}
# 调试流程:
# 1. VS Code 点"开始调试"(监听 9003 端口)
# 2. 浏览器访问页面(装 Xdebug helper 扩展激活)
# 3. 代码执行到断点处暂停,可查看变量、调用栈
# 4. F10 单步跳过、F11 单步进入、F5 继续执行
Xdebug 调试的三板斧:断点(在某行暂停)、单步(一行一行执行观察变化)、调用栈(看是怎么一层层调用到这里的)。尧图曾有个缓存失效的 bug——代码逻辑看着没问题,但缓存就是不命中。用 Xdebug 单步走一遍,发现是某个中间变量被意外覆盖,肉眼根本看不出来。注意 Xdebug 会拖慢性能,生产环境绝不能开。
二、日志分级与 Monolog
日志不是"想起来了写一条",而是分级别、有结构、可检索的。PSR-3 日志规范定义了八个级别,从紧急到调试,不同级别对应不同严重程度。生产环境只记录 warning 以上,开发环境记录 debug 才有用。PHP 生态里 Monolog 是事实标准,几乎所有框架都用它。
// Monolog 基础用法
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
$logger = new Logger('ldpk');
// 按天滚动日志文件,保留30天(防日志撑爆磁盘)
$logger->pushHandler(new RotatingFileHandler(
__DIR__ . '/logs/app.log',
30, // 保留30天
Logger::WARNING // 只记录 WARNING 及以上
));
// 八个日志级别(从高到低)
$logger->emergency('数据库连不上,站点不可用'); // EMERGENCY:系统不可用
$logger->alert('支付回调验签失败'); // ALERT:必须立即处理
$logger->critical('订单状态异常: {order}', ['order' => $id]); // CRITICAL
$logger->error('接口调用失败: {msg}', ['msg' => $e->getMessage()]); // ERROR
$logger->warning('缓存命中率低: {rate}%', ['rate' => 30]); // WARNING
$logger->notice('用户登录: {uid}', ['uid' => $userId]); // NOTICE
$logger->info('文章发布: {id}', ['id' => $postId]); // INFO
$logger->debug('SQL: {sql}', ['sql' => $query]); // DEBUG
日志要带"上下文"——光写"订单异常"没用,得带上订单号、用户 ID、异常信息。Monolog 的第二个参数就是上下文,会被序列化成 JSON 跟在日志后面。排查问题时靠 grep 订单号就能把相关日志全捞出来。尧图规定:所有 catch 块必须记日志,且日志必须包含异常堆栈($e->getTraceAsString()),否则排查时只能靠猜。
三、异常处理与上报
生产环境不能把错误直接显示给用户(既难看又泄露信息),要捕获所有未处理异常,记录日志后给用户友好的提示。同时把严重错误上报到监控平台(如 Sentry),第一时间收到告警而非等用户投诉。
// 全局异常处理器
class ExceptionHandler {
function handle(Throwable $e) {
// 1. 记录日志(带完整堆栈)
$this->logger->error('未处理异常: {msg}', [
'msg' => $e->getMessage(),
'file' => $e->getFile() . ':' . $e->getLine(),
'trace' => $e->getTraceAsString(),
'url' => $_SERVER['REQUEST_URI'] ?? '',
'uid' => $_SESSION['user_id'] ?? 0
]);
// 2. 上报到 Sentry(生产环境,实时告警)
if (env('APP_ENV') === 'production') {
\Sentry\captureException($e); // 异步上报,不阻塞
}
// 3. 给用户友好提示(不泄露技术细节)
if ($e instanceof BusinessException) {
return json(['code' => $e->getCode(), 'msg' => $e->getMessage()]);
} else {
return json(['code' => 500, 'msg' => '系统繁忙,请稍后重试']);
}
}
}
// 业务异常类(用于可控的业务错误,如"库存不足")
class BusinessException extends RuntimeException {}
// 注册全局处理器
set_exception_handler([new ExceptionHandler(), 'handle']);
区分"业务异常"和"系统异常"很重要——前者是用户操作导致的预期错误(库存不足、余额不够),记日志但不需要告警;后者是代码 bug(空指针、SQL 出错),必须告警让开发者修。不区分的话告警会被业务异常淹没,真正的问题被淹没。尧图用 Sentry 后,线上 bug 平均发现时间从"用户投诉后"缩短到"3 分钟内告警",运维压力大幅下降。这套调试 + 日志 + 异常上报的组合拳,是站点稳定运行的基础保障。