ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP8.0升级后怎么检查错误日志

PHP8.0升级后怎么检查错误日志 前言从 PHP 7.x 升到 8.0 之后最典型的三种症状是白屏页面什么都没有display_errors又是关的连一条错误都看不到日志暴涨升级前日志一天几十行升级后一天几十万行全是Deprecated和Warning失灵以前用压住的写法突然开始报致命错误脚本直接中断原来的警告 返回 null变成了异常strlen([])这种老代码从悄悄返回 null变成抛TypeError被最外层的catch (Exception $e)漏掉直接 500。原因是 PHP 8.0 动了好几处与错误处理直接相关的默认行为默认错误级别、内部函数的类型检查、致命错误的可捕获性、以及抑制符的作用范围。这些变化不会让代码编译不过只会让运行期的错误以全新的形态出现。本文讲四件事PHP 8.0 到底改了哪些错误相关的行为、各种运行模式下日志到底在哪、怎么把错误变成可检索的结构、以及一段可以直接跑的升级体检脚本。一、PHP 8.0 改了哪些与错误有关的行为先看对照表这张表就是升级后为什么日志突然不一样的答案。行为PHP 7.xPHP 8.0 起默认error_reportingE_ALL ~E_DEPRECATED ~E_STRICTE_ALL内部函数参数类型不符Warning函数返回null抛TypeError未定义常量Warning7.2 起并当作字符串用抛Error致命错误直接中断不可捕获是Error的子类可被catch (Throwable)捕获抑制符抑制所有错误不再抑制致命错误数字与字符串比较abc 0为true为false数组转字符串NoticeWarningmatch无匹配分支—抛UnhandledMatchError第一行最容易被忽略默认级别从不含弃用改成了E_ALL。也就是说同一份代码在 7.x 上跑得好好的升级后弃用信息会突然出现在日志里——不是代码变差了是默认值变了。对升级来说这其实是好事它把本来就该看的弃用信息推到了你面前。同时PHP 8.0 移除了一批函数调用它们会直接Fatal error: Call to undefined functioncreate_function()、each()、money_format()、get_magic_quotes_gpc()、hebrevc()、convert_cyr_string()、fgetss()。升级后第一件事就是用function_exists()在日志里扫一遍这几个名字。顺带记一个 PHP 8.0 新增的、写日志时非常好用的函数get_debug_type()。它比gettype()更具体对象返回完整类名而不是object比get_class()更安全对非对象不会报错。值gettype()get_debug_type()1integerint1.0doublefloattruebooleanboolnullNULLnullnew Foo()objectFoo完整类名文件句柄resourceresource (stream)二、日志到底在哪里升级后改了 php.ini 却感觉没生效绝大多数情况是看错了日志文件。按运行模式对号入座运行模式日志位置怎么确认内置服务器 / CLI标准错误stderr直接看终端输出或php -r ... 2err.logPHP-FPMphp.ini的error_log或 pool 配置里的php_admin_value[error_log]看 phpinfo() 的error_log那一行Nginx FPMNginx 的error_log只记网关错误PHP 的错误在 FPM 日志里两边都要看Apache mod_phpErrorLog指令指定的文件apache2ctl -S看配置来源Docker容器的 stderrdocker logs容器内 PHP 的 stdout/stderr 就是容器日志systemdjournalctl -u php8.0-fpm -f服务名按实际版本改几个常用的确认命令# 当前 CLI 用的 php.ini 在哪 php --ini # 打印与错误相关的所有配置当前值 php -i | grep -Ei ^(error_reporting|display_errors|log_errors|error_log|log_errors_max_len) # 实时跟踪 FPM 的错误日志 tail -f /var/log/php-fpm/www-error.log # 只筛出致命错误和未捕获异常 grep -E Fatal error|Uncaught /var/log/php-fpm/www-error.log | tail -n 50最关键的坑php -i打印的是CLI的配置而网站跑的是FPM的配置两者读的可能是完全不同的php.ini。升级后觉得配置明明改了先确认你tail的那个文件真的是 FPM 在写的那个。另外 FPM 里子进程的输出默认被丢弃需要在 pool 配置里打开catch_workers_output yes才能进主日志; php-fpm.d/www.conf catch_workers_output yes php_admin_value[error_log] /var/log/php-fpm/www-error.log php_admin_flag[log_errors] on php_admin_flag[display_errors] off注意 pool 配置里的php_admin_value/php_admin_flag优先级高于 php.ini而且不能用ini_set()在运行时覆盖。所以我在代码里写了ini_set(display_errors, 0)为什么没用答案通常就在这里。三、把错误变成可检索的结构光有日志文件不够。PHP 默认的错误行是纯文本混着各种格式很难筛、很难统计。更有效的做法是用三个钩子把错误统一成结构化日志钩子负责什么注意set_error_handler()Warning/Notice/Deprecated抓不到E_ERROR、E_PARSE、E_CORE_ERROR、E_COMPILE_ERRORset_exception_handler()未捕获的Throwable覆盖默认的 Uncaught ... Fatal error 输出register_shutdown_function()最后兜底用error_get_last()拿致命错误这是唯一能看到致命错误的途径三者配合才能覆盖全部错误。另外记住Throwable这个公共接口是PHP 7.0引入的它同时覆盖Error和Exception两条继承链——升级后catch (Exception $e)抓不到TypeError必须换成catch (Throwable $e)。四、实战统一错误采集器 升级体检下面这份采集器把上面三个钩子装进一个类输出 JSON Lines一行一条 JSON方便直接喂给日志系统做聚合。最低版本PHP 8.0。?php declare(strict_types1); /** * PHP 8.0 统一错误采集器 * 最低版本PHP 8.0用到 get_debug_type() 和构造器属性提升 */ final class ErrorCollector { /** 这些级别会被转成异常走统一处理 */ private const FATAL_LEVELS [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]; public function __construct( private string $logFile, private bool $throwOnWarning true, ) {} public function register(): void { set_error_handler([$this, handleError]); set_exception_handler([$this, handleException]); register_shutdown_function([$this, handleShutdown]); } public function handleError(int $severity, string $message, string $file , int $line 0): bool { // 被 或 error_reporting 屏蔽的交回 PHP 默认处理 if (!(error_reporting() $severity)) { return false; } // 弃用信息只记录不抛异常否则线上会因为一条 Deprecated 直接 500 if ($severity E_DEPRECATED || $severity E_USER_DEPRECATED) { $this-write(deprecated, [ message $message, file $file . : . $line, ]); return true; } if ($this-throwOnWarning) { throw new ErrorException($message, 0, $severity, $file, $line); } $this-write(warning, [message $message, file $file . : . $line]); return true; } public function handleException(Throwable $e): void { $this-write(exception, [ // get_debug_type() 是 PHP 8.0 新增的对象给完整类名标量给 int/float/bool class get_debug_type($e), message $e-getMessage(), file $e-getFile() . : . $e-getLine(), trace $e-getTraceAsString(), ]); if (!headers_sent()) { http_response_code(500); } } public function handleShutdown(): void { $last error_get_last(); if ($last null || !in_array($last[type], self::FATAL_LEVELS, true)) { return; } $this-write(fatal, [ message $last[message], file $last[file] . : . $last[line], ]); } private function write(string $channel, array $context): void { $line json_encode( [time date(c), channel $channel] $context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR ); // error_log() 的第二个参数为 3 表示追加到指定文件 // 它不受 log_errors 开关影响也不负责加换行所以要自己补 PHP_EOL error_log($line . PHP_EOL, 3, $this-logFile); } } // ---------------- 使用示例 ---------------- $collector new ErrorCollector(__DIR__ . /app-error.log, throwOnWarning: true); $collector-register(); // 1) PHP 8.0 起内部函数参数类型不符会抛 TypeError7.x 只是 Warning 返回 null try { strlen([]); } catch (TypeError $e) { printf(捕获 TypeError: %s\n, $e-getMessage()); } // 2) 数字与字符串比较的行为变了7.x 下 abc 0 为 true var_dump(abc 0); // PHP 8.0 起false // 3) match 无匹配会抛 UnhandledMatchError继承自 Error try { $x match (x) { 1 one }; } catch (UnhandledMatchError $e) { printf(捕获 UnhandledMatchError: %s\n, $e-getMessage()); } // 4) 用 get_debug_type() 打到日志里比 gettype() 有用得多 $collector-handleException(new InvalidArgumentException(演示用异常)); echo file_get_contents(__DIR__ . /app-error.log);配套的升级体检脚本把当前环境是否已经适配 8.0逐项打出来?php declare(strict_types1); /** * PHP 8.0 升级自检 * 用法php p22-upgrade-check.php */ $checks [ [error_reporting 是全量 E_ALL, error_reporting() E_ALL, → 检查 ini 的 error_reporting], [display_errors 已关闭, in_array((string) ini_get(display_errors), [0, ], true), → 生产必须关闭避免堆栈泄露给用户], [log_errors 已打开, ini_get(log_errors) 1, → 打开才会记录 PHP 自身的错误], [error_log 已指定文件, (string) ini_get(error_log) ! , → 未指定时走 SAPI 默认通道], [create_function 已不存在, !function_exists(create_function), → PHP 8.0 已移除], [each 已不存在, !function_exists(each), → PHP 8.0 已移除], [money_format 已不存在, !function_exists(money_format), → PHP 8.0 已移除], [get_debug_type 可用, function_exists(get_debug_type), → PHP 8.0 新增可替代 gettype], [json_validate 可用, function_exists(json_validate), → 需要 PHP 8.38.0 上不要用], ]; $failed 0; foreach ($checks as [$name, $ok, $hint]) { if (!$ok) { $failed; } printf([%s] %s %s\n, $ok ? OK : FAIL, $name, $ok ? : $hint); } printf(\nPHP %s (%s)共 %d 项失败 %d 项\n, PHP_VERSION, PHP_SAPI, count($checks), $failed);把这两个脚本在CLI和FPM下各跑一次FPM 下通过一个临时路由访问对照输出的差异就能确认我改的配置到底有没有生效。常见坑点1. 以为还能压住一切❌ 错误写法?php $content file_get_contents($logPath); // 路径不存在时8.0 起致命错误照样中断✅ 正确写法?php try { $content file_get_contents($logPath); } catch (Throwable $e) { // 8.0 起致命错误是 Error 对象能被捕获 error_log(读取失败: . $e-getMessage()); $content ; }2. 用catch (Exception $e)兜底❌ 错误写法?php try { $len strlen($input); } catch (Exception $e) { // TypeError 继承自 Error不是 Exception抓不到 // 永远进不来 }✅ 正确写法?php try { $len strlen($input); } catch (Throwable $e) { // Throwable 是 PHP 7.0 引入的公共接口 // Error 和 Exception 都能抓到 }3. 把弃用信息也抛成异常❌ 错误写法set_error_handler里对所有级别一律throw new ErrorException(...)。结果线上一个Deprecated就把整个接口打成 500而且报错内容与业务毫无关系。✅ 正确写法弃用只记录不抛异常见上一节采集器里的E_DEPRECATED分支。把升级后要清理的清单和要立刻中断的错误分开对待。4. 以为log_errorsOff会让error_log()不写❌ 错误理解关掉log_errors就能让代码里所有error_log()静音。✅ 事实log_errors只控制PHP 自身的错误往哪写error_log()是一个函数它按自己的参数干活尤其error_log($msg, 3, $file)这种追加到文件的形式完全不受log_errors影响。要静音只能改代码或换消息类型。5. 在代码里ini_set想覆盖 FPM 的 pool 配置❌ 错误写法?php ini_set(display_errors, 0); // pool 里写了 php_admin_flag[display_errors]on 时无效✅ 正确写法php_admin_value/php_admin_flag优先级高于ini_set()改配置要去php-fpm.d/*.conf。要确认最终生效值跑一个只输出phpinfo()的临时页面看别信php -i的输出——那是 CLI 的。6. 只 tail Nginx 日志❌ 错误做法升级后 500只盯着/var/log/nginx/error.log看里面只有一句FastCGI sent in stderr: PHP message: ...或者什么都没有。✅ 正确做法Nginx 的日志是网关视角PHP 的错误在 FPM 侧。要么查 FPM 的error_log要么在 pool 里打开catch_workers_output yes把子进程输出收进主日志。7. 记录日志时json_encode返回false日志行变成空的❌ 错误写法?php error_log(json_encode($context) . PHP_EOL, 3, $logFile); // $context 里混入非法 UTF-8 时json_encode 返回 false日志里只剩一个空行✅ 正确写法?php $line json_encode($context, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_UNICODE); error_log(($line false ? {error:encode_failed} : $line) . PHP_EOL, 3, $logFile);JSON_PARTIAL_OUTPUT_ON_ERROR能让编码尽量完成用UFFFD顶替坏字节——比丢掉整条日志强。8. 在register_shutdown_function里访问不存在的变量❌ 错误写法?php register_shutdown_function(function () use ($e) { error_log($e-getMessage()); // 闭包外的 $e 根本不存在作用域里拿不到 });✅ 正确写法致命错误只能通过error_get_last()取?php register_shutdown_function(function (): void { $last error_get_last(); if ($last ! null in_array($last[type], [E_ERROR, E_PARSE, E_COMPILE_ERROR], true)) { error_log(Fatal: {$last[message]} {$last[file]}:{$last[line]}); } });总结升级后遇到的现象根本原因处理方向日志里突然全是Deprecatederror_reporting默认变成E_ALL把弃用单独收集统计不要混进错误处理Call to undefined function8.0 移除了each/create_function/money_format等function_exists()全量扫描后替换老代码突然 500内部函数类型不符由Warning变成TypeError用catch (Throwable)兜底修掉类型错误的调用压不住的致命错误8.0 起不再抑制致命错误用try/catch加register_shutdown_function改了 php.ini 不生效php_admin_value覆盖或看的是 CLI 的配置用phpinfo()确认 FPM 的最终生效值白屏日志里什么都没有日志看错了文件或 FPM 丢弃了子进程输出catch_workers_output、查 FPM 自己的error_log升级 PHP 8.0 之后的第一件事不是改代码而是确认错误看得见error_reportingE_ALL、log_errorsOn、display_errorsOff、三个错误钩子全部注册。等日志能稳定地把TypeError、Deprecated、UnhandledMatchError分门别类地记下来升级剩下的工作就只是照着清单一条条改了。
返回列表