Fatal error: Uncaught TypeError 之后,我养成了「三秒看栈、十秒定位」的肌肉记忆

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 92 浏览 0 回复

写插件最烦的不是报错本身,是报错信息把你往反方向带。上周有个兄弟在群里甩了张图:Fatal error: Uncaught TypeError: Argument 1 passed to MyPlugin\Tracker::record() must be of the type array, null given,下面跟了三十多行栈追踪。他盯着 record() 方法看了二十分钟,其实真凶在八层调用之外。

这篇不罗列所有 Exception 类型,只聊我实战中攒下的「报错→定位」条件反射,按报错形态分三类。

一、TypeError / ArgumentCountError:别在案发地点找凶手

PHP 7+ 的严格类型让这类报错爆炸式增长。新手常犯的错误:顺着栈顶往下读,把最后一帧当犯罪现场。

实际该看的顺序:

  1. 先看第一帧(栈顶):确认哪个函数被调崩了、期望什么类型
  2. 最后一帧(栈底):谁最先发起的调用?传入的变量从哪来?
  3. 中间帧挑自己代码的边界看:插件钩子、filter 回调、Ajax 入口这些"接口层"

举个真事。我的定时任务回调大概长这样:

add_action( 'myplugin_daily_sync', [ $this->syncer, 'run' ] );

// Syncer 类
public function run( array $config ) {
    $this->tracker->record( $config );
}

报错说 record() 要 array 给了 null。但 run()$config 哪来的?WP Cron 的 action 调度是 do_action( 'myplugin_daily_sync' )不带参数。所以 $config 是 null,一路传到底才炸。

定位口诀:TypeError 找源头,ArgumentCountError 找调度。WP 的钩子机制让参数传递变成"暗渠",栈追踪里看不到 do_action 传了啥,得去 wp_schedule_event 的注册点核对参数签名。

二、RuntimeException / 自定义异常:被吞掉的「中间层」

有些插件框架爱包 try-catch,把异常转成 WP_Error 或者干脆记个日志继续跑。结果页面白屏没报错,数据却不对,你以为是逻辑 bug,其实是异常被吃了。

我现在的习惯:在开发环境给 wp-content/debug.log 加一道「异常哨兵」:

set_exception_handler( function( $e ) {
    if ( $e instanceof \RuntimeException ) {
        error_log( '[RUNTIME] ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString() );
        // 开发环境直接抛,别吞
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            throw $e;
        }
    }
} );

重点盯两类自定义异常:

  • HTTP 层封装异常:比如把 Guzzle 的 ConnectException 包成 MyPlugin\ApiException,但丢了 getHandlerContext() 里的 DNS/SSL 细节
  • 数据库事务回滚异常$wpdb->query( 'ROLLBACK' ) 自己失败不会抛,但你的业务异常已经被 catch 过了,两头空

定位技巧:在 catch 块里强制 throw $e 重跑一遍,看原始异常链。PHP 8 的 $e->getPrevious() 一定要链式追到底。

三、Error(非 Exception):编译期陷阱与隐式加载

这类最阴,因为 try-catch( Exception $e ) 抓不住 Error,得用 catch( Throwable $t )

高频踩坑场景:

报错表面位置实际雷区
Class 'MyPlugin\Vendor\SomeLib' not foundnew 语句composer autoload 没更新,或 PSR-4 路径大小写和类名对不上(Linux 生产机才炸)
Cannot declare class MyPlugin\Admin, because the name is already in userequire_once同文件被不同路径引用(软链接、大小写别名),_once 的哈希判断失效
Allowed memory size exhausted任意位置WP_Query 没加 'no_found_rows' => true,或者循环里重复 new WP_Query 没 reset

最后这个内存溢出,我有一次追了两个小时,发现是 register_activation_hook 里批量插入测试数据,用了 wp_insert_post() 但每插一条都触发 save_post,我的回调又调了 wp_update_post(),死循环到内存炸。栈追踪最后一帧在 wpdb->insert(),完全无关。

我的「三秒十秒」流程

现在看到报错,手比脑子快:

  • 0-3 秒:扫报错类名(TypeError/Error/RuntimeException?),扫文件路径(核心/主题/插件/ vendor?),判断「这是谁的锅」
  • 3-10 秒:TypeError 看栈底调用源;Error 看 autoload/文件系统;RuntimeException 看有没有被包 try-catch
  • 10 秒后:还没头绪,直接 grep -r "那个函数名" --include="*.php" 全项目搜调用点,往往比读栈快

有个细节:WP 的 wp_die() 在 Ajax 请求里会套 HTML,把栈追踪埋进注释。按 F12 看响应原文,比盯着「0」状态码瞎猜强十倍。

你们有没有被报错信息「指东打西」坑过的经历?或者哪种 Exception 的定位套路特别反直觉?

评论0
回复 · 0
还没有回复
微信客服 微信客服