PHP 调试与日志
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
PHP 的默认错误处理对新手很不友好:白屏、什么都不显示,或者把堆栈直接吐给用户。这一篇把「看得见」和「不泄露」两件事同时解决。
环境准备
示例在 PHP 8.4 下验证。开发环境建议装 Xdebug 3,php -v 能看到 with Xdebug 即成功。
两套配置
开发时把错误打全,生产时只写日志:
// config/error.dev.php
declare(strict_types=1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
// config/error.prod.php
declare(strict_types=1);
error_reporting(E_ALL);
ini_set('display_errors', '0'); // 绝不输出到页面
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/../storage/logs/php-error.log');
用环境变量选择加载哪一份:
$env = getenv('APP_ENV') ?: 'prod';
require __DIR__ . "/config/error.{$env}.php";
display_errors=1 在生产环境会把文件路径、SQL、密钥片段暴露给任何访问者,也是很多扫描器的第一入口。
捕获错误与异常
错误(E_WARNING 之类)和异常(Throwable)是两套机制,都要接管:
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
if (!(error_reporting() & $severity)) {
return false; // 被 @ 抑制的,交给默认处理
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
set_exception_handler(function (Throwable $e): void {
error_log(sprintf(
"[%s] %s: %s in %s:%d\n%s",
date('c'),
$e::class,
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString(),
));
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: text/html; charset=utf-8');
}
echo '<h1>服务器出错了</h1><p>请稍后重试。</p>';
});
register_shutdown_function(function (): void {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
error_log("[fatal] {$error['message']} in {$error['file']}:{$error['line']}");
}
});
把警告转成异常,能让「忘了检查返回值」在测试阶段就炸出来。但要排除 @ 抑制的场景,否则第三方库的容错写法会一起失效。
致命错误(解析错误、内存耗尽)走不到 set_exception_handler,只能靠 register_shutdown_function + error_get_last() 记录。
结构化日志
function log_event(string $level, string $message, array $context = []): void
{
$record = [
'time' => date('c'),
'level' => $level,
'message' => $message,
'request' => $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8)),
'context' => $context,
];
error_log(json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
log_event('info', '文章已发布', ['post_id' => 42, 'user_id' => 7]);
输出一行一个 JSON,grep 和日志系统都能直接消费。请求 ID 从入口生成并回写到响应头,前端报错时带上它,排查能直接定位:
$requestId = $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8));
header("X-Request-Id: {$requestId}");
不要记录密码、完整身份证、支付卡号、会话 ID。需要关联用户时记 ID。
日志分级与文件
enum LogLevel: string
{
case Debug = 'debug';
case Info = 'info';
case Warning = 'warning';
case Error = 'error';
}
debug:只在开发环境开,生产关闭。info:关键业务动作(登录、下单、发布)。warning:可恢复的异常(重试成功、降级返回)。error:需要人介入的失败。
按天切分,避免单个文件无限增长:
$file = __DIR__ . '/../storage/logs/app-' . date('Y-m-d') . '.log';
error_log($line . "\n", 3, $file);
权限设为仅应用用户可写,Web 根目录下的日志文件要禁止直接访问(Nginx/Apache 配 deny)。
Xdebug 断点调试
php.ini 关键配置(Xdebug 3):
xdebug.mode=debug,develop
xdebug.start_with_request=yes
xdebug.client_port=9003
xdebug.idekey=VSCODE
配好 IDE 的监听后,在编辑器里点断点,刷新页面即停在断点处,可以看局部变量、调用栈、单步执行。
临时排查也可以不装 Xdebug:
var_dump($value); // 输出类型与结构
error_log(print_r($value, true));
debug_zval_dump($value); // 引用计数,排查引用问题
查看执行时间:
$start = microtime(true);
// ... 代码
error_log('耗时 ' . round((microtime(true) - $start) * 1000, 2) . ' ms');
白屏排查顺序
- 看
display_errors是否关闭、error_log路径是否可写; - 命令行直接跑
php public/index.php,绕过 Web 服务器看真实报错; - 检查
php -l语法(E_PARSE不进日志时尤其有用); - 看 Web 服务器错误日志(Nginx 的
error_log、Apache 的error_log); - 检查内存限制与执行时间:
memory_limit、max_execution_time。
400/500 同时出现且页面空白,九成是致命错误没有显示出来。
生产环境的错误页
// 入口处提前注册,避免半渲染状态
if ($env === 'prod') {
ini_set('display_errors', '0');
}
错误页本身也要简单:不要查数据库、不要渲染菜单。静态 HTML 最稳。
常见错误与注意事项
1. 用 var_dump 调试后忘了删
生产环境把内部结构输出给用户。提交前搜一遍 var_dump / print_r,或用 lint 规则拦。
2. @ 抑制一切
@$result = $obj->method() 把致命问题也吞掉。改成显式检查返回值或 try/catch。
3. 日志里写敏感信息
包括异常消息里拼进去的 SQL 参数。记录前过滤,或用白名单字段。
4. 只记 $e->getMessage()
没有文件和行号,等于没记。至少记 $e::class、getFile()、getLine()。
5. 日志文件放在 Web 可访问目录
/logs/app.log 能被直接下载。放 Web 根目录之外,或加服务器规则拒绝。
6. 时区不一致
date_default_timezone_set('Asia/Shanghai') 或 date.timezone 配置统一,否则日志时间对不上应用数据。
7. 用 echo 当调试手段
输出会混进页面/JSON,破坏响应格式。调试信息走 error_log,只在明确知道后果时用 echo。
小结
- 开发开
display_errors,生产只写日志并返回通用错误页。 - 错误与异常分别接管:
set_error_handler、set_exception_handler、register_shutdown_function。 - 日志一行一条 JSON,带请求 ID,禁止写入敏感字段。
- Xdebug 断点优于到处
var_dump;临时排查用error_log(print_r(..., true))。 - 白屏先看日志与
php -l,再看 Web 服务器与资源限制。