朋友们,今天我们要讨论的是每个曾经深入研究过别人PHP代码的人都会抓狂的事情。你打开一个网站,什么都没有。一个空白屏幕。没有错误,没有关于出了什么问题的线索。

听起来熟悉吗?

这是PHP致命错误的典型情况,它只是杀死脚本,然后不告而别。今天,我们将学习如何让PHP说话,启用日志,并找到那些隐藏在"白色死亡屏幕"背后的致命错误。

PHP为何沉默:一点理论

首先,让我们了解PHP通常如何处理错误。这不仅仅是"错误 -> 日志"。有一个完整的层次结构。

在PHP 7+中,所有可能出错的东西都实现了 Throwable 接口。它有两个主要继承者: - Error — PHP内部错误(致命错误、解析错误、类型错误) - Exception — 可以通过try-catch捕获的异常

致命错误(E_ERROR、E_PARSE、E_CORE_ERROR)是每个开发人员最可怕的噩梦。它们会立即杀死脚本,通过 set_error_handler() 的标准处理程序无法捕获它们 。拦截致命错误的唯一方法是使用 register_shutdown_function()

但我们一步一步来。首先,我们需要启用日志记录,以便错误确实被写入某个地方。

第1步:通过php.ini启用日志记录

配置日志的最正确方法是编辑PHP配置文件。它位于哪里取决于您的系统:

| 系统 | php.ini的路径 | |---------|---------------| | Ubuntu/Debian (Apache) | /etc/php/7.x/apache2/php.ini | | Ubuntu/Debian (CLI) | /etc/php/7.x/cli/php.ini | | Ubuntu/Debian (PHP-FPM) | /etc/php/7.x/fpm/php.ini | | CentOS/RHEL | /etc/php.ini/etc/php.d/ | | Windows (XAMPP) | C:\xampp\php\php.ini | | Windows (WAMP) | C:\wamp\bin\php\phpX.X.X\php.ini |

在这个文件中,我们关心以下指令:

```ini ; 启用将错误写入日志文件(必须!) log_errors = On

; 指定日志文件的路径 error_log = /var/log/php_errors.log

; 禁用在屏幕上显示错误(用于生产环境!) display_errors = Off

; 对于开发,您可以启用此选项,但之后禁用它 display_startup_errors = Off

; 错误报告级别 — 所有错误 error_reporting = E_ALL ```

重要提示: 命令行界面(CLI)使用自己的php.ini,日志可能写入不同的位置 。如果您在控制台中测试脚本,请单独检查。

更改后,您需要重新启动Web服务器: bash sudo systemctl restart apache2 # 对于Apache sudo systemctl restart php7.4-fpm # 对于PHP-FPM

这些设置的含义

  • log_errors = On — 启用将错误写入文件。没有这个,PHP会像坟墓一样沉默 。
  • error_log — 将写入所有内容的文件的路径。Web服务器用户(通常是 www-dataapache)对此文件及其目录具有写入权限至关重要 。
  • display_errors = Off — 在生产环境中务必禁用它,以避免向用户暴露详细信息 。
  • error_reporting = E_ALL — 记录所有类型的错误。对于生产环境,您可以限制此设置,但对于调试,最好看到一切。

第2步:替代方案:直接在代码中启用日志记录

如果您无权访问php.ini(例如,在共享主机上),您可以尝试使用 ini_set() 函数直接在脚本中启用日志记录。

将此插入问题文件的最开头:

```php

/dev/null ``` 或者创建一个包含 `phpinfo();` 的PHP文件,并查找 **error_log** 部分。 ## 第4步:读取日志:工具和技术 一旦日志被写入,您需要知道如何快速查看和过滤它们。 ### tail — 查看最新事件 ```bash # 最后50行 tail -n 50 /var/log/php_errors.log # 实时跟踪(如docker logs -f) tail -f /var/log/php_errors.log ``` ### grep — 搜索特定错误 ```bash # 仅致命错误 grep "Fatal error" /var/log/php_errors.log # 解析错误(语法错误) grep "Parse error" /var/log/php_errors.log # 忽略大小写 grep -i "warning" /var/log/php_errors.log ``` ### less — 方便的导航式查看 ```bash less /var/log/php_errors.log ``` 在less中,您可以按 `/` 并输入一个词进行搜索,`n` 下一个匹配项,`N` 上一个匹配项。 ### 组合 — 强大的管道 ```bash # 显示最后100行并搜索错误 tail -n 100 /var/log/php_errors.log | grep -i fatal # 实时跟踪并过滤 tail -f /var/log/php_errors.log | grep --line-buffered "error" # 按类型计数错误 grep -o "PHP [A-Za-z]* error" /var/log/php_errors.log | sort | uniq -c ``` ## 第5步:解读错误:是什么 典型的日志条目如下所示: ``` [2026-03-04 14:35:22 UTC] PHP Fatal error: Uncaught Error: Call to undefined function foo() in /var/www/html/index.php on line 10 ``` 我们来分解一下: - **日期和时间** — 发生时间 - **PHP Fatal error** — 错误级别 - **Uncaught Error: Call to undefined function foo()** — 消息 - **/var/www/html/index.php:10** — 文件和行号 主要错误级别: | 级别 | 含义 | 示例 | |------|------|------| | **E_ERROR** / **Fatal error** | 致命错误,脚本终止 | 调用不存在的函数,内存不足 | | **E_WARNING** / **Warning** | 警告,但脚本继续 | `include` 不存在的文件 | | **E_PARSE** / **Parse error** | 语法错误,脚本不会运行 | 缺少分号 | | **E_NOTICE** / **Notice** | 通知,通常不严重 | 访问未定义的变量 | | **E_DEPRECATED** | 已弃用的构造 | 使用 `mysql_connect()` | ## 第6步:捕获致命错误(有趣的部分) 致命错误是那些无法被标准 `set_error_handler()` 捕获的错误。它们会立即杀死脚本,默认处理程序根本没有时间执行 。 但有一种方法!使用 `register_shutdown_function()` — 无论发生什么,即使在致命错误时,也会调用此函数。 ### 简单的致命错误处理程序 ```php getMessage()." | ".$e->getFile().":".$e->getLine()." | $url\n"; file_put_contents($logFile, $msg, FILE_APPEND); }); // 致命错误的处理程序 register_shutdown_function(function() use ($logFile) { $error = error_get_last(); if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { $url = ($_SERVER['HTTP_HOST'] ?? 'CLI') . ($_SERVER['REQUEST_URI'] ?? ''); $msg = "[".date('Y-m-d H:i:s')."] [FATAL] ".$error['message']." | ".$error['file'].":".$error['line']." | $url\n"; file_put_contents($logFile, $msg, FILE_APPEND); } }); ``` 您可以将此代码插入主文件(例如 `index.php` 或 `header.php`)的开头,它将捕获绝对所有内容 。 ## 第7步:流行框架中的日志 如果您使用现代框架,日志记录已经设置好,但日志路径不同。 ### Laravel 日志存储在 `storage/logs/laravel.log` 中。默认情况下,它使用Monolog;格式可以是文本或JSON。要查看错误: ```bash tail -f storage/logs/laravel.log ``` ### Symfony 默认日志:`var/log/dev.log` 和 `var/log/prod.log`。也使用Monolog,高度可配置。 ### WordPress 默认情况下,WordPress不写日志,但您可以通过在 `wp-config.php` 中添加以下内容来启用它们: ```php define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); ``` 日志将在 `/wp-content/debug.log` 中。 ## 第8步:常见问题及其解决方案 ### 问题:日志根本没有写入 要检查什么: 1. `log_errors = On` 是否确实设置了? 2. 文件及其目录是否有写入权限? 3. 您编辑了正确的php.ini吗(用 `phpinfo()` 检查)? 4. 更改后是否重新启动了Web服务器? ### 问题:致命错误没有被捕获 即使启用了日志记录,如果致命错误发生得太早(例如,在您启用日志记录的同一文件中出现解析错误),有时也不会被写入。唯一的解决方案是正确放置 — 在最开头,在任何其他内容之前 。 ### 问题:日志太多 如果在生产环境中启用了 `E_ALL`,日志会快速增长。合理地配置 `error_reporting` : ```ini error_reporting = E_ALL & ~E_DEPRECATED & ~E_NOTICE ``` ### 问题:日志填满磁盘 使用 `logrotate` 设置日志轮转。PHP日志的配置: ``` /var/log/php_errors.log { daily rotate 30 compress delaycompress missingok notifempty create 644 www-data www-data } ``` ## 第9步:实践示例:调查空白页面 想象一下这种情况:您打开一个网站,它一片空白。逐步做什么? 1. **检查Web服务器日志**(Apache/Nginx)— 通常包含线索 2. **检查PHP日志** — 查找过去几分钟的条目 ```bash tail -n 50 /var/log/php_errors.log | grep -A5 -B5 "$(date +%Y-%m-%d)" ``` 3. **找到错误**: ``` [2026-03-04 15:30:22] PHP Fatal error: Uncaught Error: Class 'DB' not found in /var/www/site/index.php on line 12 ``` 4. **打开文件**,检查第12行 — 确实,您忘记包含类了。 5. **修复**,测试 — 网站工作了! 神奇! ## 快速参考:备忘单 | 操作 | 命令 / 代码 | |------|-------------| | 在php.ini中启用日志记录 | `log_errors = On` + `error_log = /文件/路径` | | 在代码中启用 | `ini_set('log_errors', 'On');` | | 查看最后几行 | `tail -f /var/log/php_errors.log` | | 搜索致命错误 | `grep "Fatal error" /var/log/php_errors.log` | | 捕获致命错误 | `register_shutdown_function()` + `error_get_last()` | | Laravel日志在哪里? | `storage/logs/laravel.log` | | Symfony日志在哪里? | `var/log/dev.log` 和 `var/log/prod.log` | ## 代替结论 PHP并不像人们常说的那么可怕。是的,"白色死亡屏幕"是可怕的,但它背后总是一个特定的错误,只是不想说话。学会启用日志,PHP就会开始说话。 主要规则: - 在生产环境中,**始终**启用 `log_errors` 并禁用 `display_errors` - 将日志写入专用文件,而不是syslog — 更容易搜索 - 使用 `register_shutdown_function` 捕获致命错误 - 定期检查日志 — 至少每天一次 请记住:完美的代码不会写错误。但如果错误发生了,至少让它留下一个便条。 ?>