[filp/whoops]让 PHP 错误页好看又好用

2026-06-16 奥古斯宏 #PHP #Whoops #错误处理 #调试 #异常
PHP 报错就是白页黑字一堆堆栈,文件路径行号摆在那但看不出问题在哪。Whoops 把错误页做成了带代码上下文、堆栈折叠、参数展开、一键跳 IDE 的可交互界面,13K Stars,Laravel 默认就是它。

写 PHP 谁没遇到过白页——display_errors 关了的,线上直接给你一张白屏;本地开着的,Fatal error: Uncaught TypeError 一坨黑底白字糊脸上,文件路径和行号是有了,但堆栈一深就只能眼睁睁数 #0 #1 #2 数到迷路。Whoops 干的事就是把这一坨错误信息变成一个能折叠、能跳转、能看每个栈帧代码上下文的网页——Filipe Dobreira 2012 年写的,现在 13K+ Star、4.25 亿+ 累计安装,Laravel 4 起就默认带着它,是 PHP 错误展示这个细分领域的事实标准。

安装

composer require filp/whoops

最新稳定版 2.18.4(2025-08),要求 PHP ^7.1 || ^8.0,只有一个依赖 psr/log。MIT 协议。如果你用 Laravel 5.5+、Mezzio、ZubZet 1.2+,它已经在 vendor 里了,不用重复装。

最小可用:5 行代码出效果

use Whoops\Run;
use Whoops\Handler\PrettyPageHandler;

$whoops = new Run;
$whoops->pushHandler(new PrettyPageHandler);
$whoops->register();

// 触发一个错误试试
throw new RuntimeException('演示一下错误页');

把这 5 行塞在入口文件最前面,访问任意会报错的页面,原来的白屏就变成了一张带代码高亮、堆栈列表、请求环境、应用数据的页面——出错那行连同前后几行代码直接渲染在错误页里,不用再回去翻 IDE。

下面是我用 TypeError 触发的实际效果(App\Order::create(int $id) 收到字符串):

Whoops 错误页:TypeError 标题、文件:行号、栈帧列表、当前帧代码上下文

顶部一行红色 TypeError 标题直接告诉你"是什么错",下面跟着文件路径和出错行号;左侧是完整调用栈,右侧是当前选中那一帧的代码上下文,出错行用红色背景高亮。整页是单个 HTML 文件,没有外部资源依赖,丢到任何 PHP 环境都能渲染。

为什么原生错误页不够用

立个靶子。下面是 PHP 原生 Fatal error 输出(开了 display_errors):

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

Fatal error: Uncaught TypeError: Argument 1 passed to App\Order::create() must be of the type int, string given, called in /var/www/app/Service.php on line 42 and defined in /var/www/app/Order.php on line 15
TypeError: Argument 1 passed to App\Order::create() must be of the type int, string given, called in /var/www/app/Service.php on line 42 and defined in /var/www/app/Order.php on line 15
Stack trace:
#0 /var/www/app/Service.php(42): App\Order::create('abc')
#1 /var/www/app/Controller.php(11): App\Service->run()
#2 /var/www/public/index.php(5): App\Controller->handle()
#3 {main}
  thrown in /var/www/app/Order.php on line 15

一堆路径和行号摆在那,但你要回答这些问题都得自己再去翻代码:

  • 出错那行前后写的什么?得切回 IDE 看
  • create('abc') 这个调用方传的参数完整值是什么?看不到
  • 哪一帧是自己的代码、哪一帧是 vendor 里的依赖?没标注
  • HTTP 请求当时带没带某个 header?得自己 var_dump($_SERVER)
  • 这个错误能不能一键在 IDE 打开那一行?想都别想

Whoops 把这些问题全部在错误页里直接答了。

PrettyPageHandler:默认就是天花板

PrettyPageHandler 是 Whoops 默认的错误页面 handler,整个库最值钱的部分。一张错误页分成几块:

  • 顶部:异常类型 + 消息 + 出错的文件:行号,可点击直接在 IDE 打开
  • Stack Frames:每一帧显示文件、行号、类/方法、参数;每帧可单独展开看代码上下文(默认前后各几行);vendor 目录里的帧默认折叠,只突出你自己应用的代码
  • Request / Server / Query / Cookie / Session 等环境数据表,点开即看
  • 应用自定义数据表(后面讲 addDataTable

每个栈帧点开能看到那段代码,意味着你不用再"切窗口 → 翻 IDE → 找行号"——错误现场一目了然。App\Order::create('abc') 这一帧点开,create 方法签名、参数、调用那行的上下文全在一个屏幕里。

这是 Whoops 的核心价值:让"排查一个错误"从多窗口切换降级成"在一个页面里点几下"

点击任意一帧就能切换到那一层的代码上下文。下面这个循环动画展示了同一个错误从 #4 帧(异常抛出点 Order.php)依次切到 #3 Service.php、#2 Controller.php、#1 / #0 index.php,右栏代码区跟着同步变化:

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

栈帧切换动画:依次点击 5 个栈帧,右侧代码区同步显示对应文件

栈帧标签是"这一帧即将进入的方法",文件路径是"调用发生的那一行"——所以左侧标签写 App\Order create 但右侧代码是 Service.php,因为 Service.php 第 15 行正在调用 Order::create。这跟 IDE 调试器的栈帧语义一致。

栈帧列表里 vendor 目录的帧默认折叠,只突出你自己应用里的代码,对定位"是我代码的锅还是依赖的锅"非常关键。

多 Handler 协作:Web / AJAX / CLI 自动适配

Whoops 的设计是 handler 栈(LIFO,后进先出)。pushHandler 越晚压入的越先执行,配合 Handler::DONE / Handler::LAST_HANDLER 两个常量,可以做"按场景挑 handler"。

最常见的组合——AJAX 请求返 JSON、CLI 走纯文本、其他走漂亮页面:

use Whoops\Run;
use Whoops\Util\Misc;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Handler\JsonResponseHandler;
use Whoops\Handler\PlainTextHandler;

$whoops = new Run;

if (Misc::isCommandLine()) {
    $whoops->pushHandler(new PlainTextHandler);
} elseif (Misc::isAjaxRequest()) {
    $json = new JsonResponseHandler;
    $json->addTraceToOutput(true);  // AJAX 错误也带完整堆栈
    $whoops->pushHandler($json);
} else {
    $whoops->pushHandler(new PrettyPageHandler);
}

$whoops->register();

注意 pushHandler 的顺序:只有压入的第一个 handler 会真正渲染输出(除非它返回 Handler::DONE 把球踢给下一个)。所以这里用 if/elseif 让"该用哪个"在注册阶段就决定清楚,比压一堆然后用 LAST_HANDLER 互踢更清晰。

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

JsonResponseHandler 的输出长这样(带 trace):

{
  "error": {
    "type": "TypeError",
    "message": "Argument 1 passed to App\\Order::create() must be of the type int, string given",
    "file": "/var/www/app/Order.php",
    "line": 15,
    "trace": [
      {"file": "/var/www/app/Service.php", "line": 42, "function": "create", "class": "App\\Order", "args": ["abc"]},
      ...
    ]
  }
}

前端 fetch / axios 拿到这个就能直接把错误展示在弹窗里,不用自己 try/catch 拼装。

setEditor:一键在 IDE 打开出错文件

这是我最喜欢的功能。PrettyPageHandler::setEditor() 把错误页上的文件路径换成可点击链接,一点就直接在 IDE 里打开出错那一行。内置支持 11 种编辑器:

$handler = new PrettyPageHandler;
$handler->setEditor('phpstorm');  // 或者 vscode / sublime / atom / idea / macvim / textmate / emacs / netbeans / espresso / xdebug

VSCode、PhpStorm 这两个最常用,开发时点一下就跳过去,比手动复制文件路径再切窗口快一个量级。

点击错误页右栏的 Open: 文件路径 链接,浏览器会弹窗询问是否允许打开 VSCode(首次需要授权,之后就直接跳):

点击 Open 链接后浏览器弹出 vscode 协议对话框,允许后 VSCode 直接打开出错文件

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

不在列表里也没事,传闭包自己拼 URL:

$handler->setEditor(function (string $file, int $line): string {
    // 远程文件映射到本地路径
    $file = preg_replace('#^/var/www/#', '/Users/me/code/', $file);
    return "vscode://file/$file:$line";
});

Laravel 项目里这一步已经默认配好了,不用手动设置。

addDataTable:把请求上下文塞进错误页

错误页自带的 $_SERVER / $_GET / $_POST 那些表只是基础。真实项目里排查问题往往还想知道:当前登录用户是谁?这条请求的 trace ID 是什么?路由命中的是哪个 controller?

addDataTable 就是给错误页加自定义数据表:

$handler = new PrettyPageHandler;

// 直接给数据
$handler->addDataTable('Current User', [
    'id'    => $user->id ?? null,
    'name'  => $user->name ?? 'guest',
    'roles' => $user->roles ?? [],
]);

// 懒加载(错误发生时才执行,避免无错误时浪费性能)
$handler->addDataTableCallback('Request Context', function () {
    return [
        'trace_id'  => request()->attributes->get('trace_id'),
        'route'     => request()->attributes->get('_route'),
        'memory'    => memory_get_usage(true),
    ];
});

这些表会渲染在错误页底部,跟原生环境表并列。我自己的项目里固定塞 trace_id当前用户,因为日志系统按 trace_id 索引,错误页直接复制 trace_id 去日志系统查上下游,闭环。

实际渲染效果(错误页底部展开 Environment & details 区域):

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

Environment & details 区域:Current User、Request Context、GET Data 等数据表并列展示

Current UserRequest Context 是我自己加的表,下面 GET Data / POST Data / Server/Request Data 是 Whoops 默认就会渲染的环境信息。

silenceErrorsInPaths:精确静音

有些路径里的 deprecated 警告就是噪音(比如某个老库在 PHP 8.4 下报一堆),但又不能整体降低 error_reportingsilenceErrorsInPaths 按路径正则+错误级别精确静默:

$whoops->silenceErrorsInPaths(
    ['#/vendor/legacy/.*#'],
    E_DEPRECATED | E_STRICT
);

调用多次会累加。我用来静音过 WordPress 集成模块的 deprecated 警告——其他代码里 E_DEPRECATED 还是照常报。

框架集成

Laravel 5.5+ 自带,开发环境默认开启,生产环境自动关。配置在 config/app.phpdebug 项,.envAPP_DEBUG=true 时启用。不用动任何代码。

接入原生项目 / 不支持 Whoops 的框架,3 行搞定:

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

// 入口文件最早的地方
$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

Slim / Symfony / Yii / CakePHP / Phalcon / Laminas 等都有社区桥接包,README 里列了完整链接,按需取用。

踩过的坑

  1. 生产环境忘关等于数据泄露。Whoops 默认会把 $_SERVER$_COOKIE$_SESSION、甚至 addDataTable 里你塞进去的所有数据完整渲染到错误页里——session token、API key、数据库密码、用户隐私一条不漏。上线前必须根据环境判断关掉,或者只让特定 IP 看到:
    if (env('APP_ENV') !== 'production' || $_SERVER['REMOTE_ADDR'] === '办公室出口 IP') {
        $whoops->register();
    }
    // 生产环境走自己的 try/catch + 日志 + 通用错误页
    
    Laravel 这块已经处理好了,但自己集成的时候必须想清楚
  2. handler 栈是 LIFOpushHandler 后压的先执行。注册顺序写反了,你以为 PrettyPageHandler 会兜底,结果被压在底下的 JsonResponseHandler 永远抢不到球。规则:先 push 兜底 handler,再 push 优先级更高的。或者直接用 prependHandler 显式插队首。
  3. register() 必须在所有其他错误处理器设置之前。Whoops 用 set_error_handler / set_exception_handler / register_shutdown_function,后注册的同类型 handler 会覆盖它。如果框架有自己的错误处理初始化逻辑,先把 Whoops register() 放进去,再让框架覆盖;或者反过来,看你想谁优先。
  4. silenceErrorsInPaths 的正则要带分隔符。文档里写 #/path/.*# 是有意的——内部用 preg_match,不写 # 会被当成普通字符串。第一次用的时候我直接传了 /vendor/legacy/,怎么都不生效,盯着日志看了一小时。
  5. sendExitCode(1) 在 PHPUnit 里会污染测试结果。Whoops 默认 CLI 报错后 exit(1),集成测试里如果意外触发错误会让整个测试进程挂掉。测试环境用 $whoops->allowQuit(false); $whoops->writeToOutput(false); 关掉,把错误转成可控的输出。
  6. addDataTableCallback 是错误发生时才执行的。别在里面依赖还没初始化的服务(比如请求还没路由前的某个 DI 容器实例),否则原始错误会被你这段 callback 自己的 NullPointer 覆盖,看不出最初到底是什么错了。

它依然是首选

13K+ Star、4.25 亿累计安装、Laravel 默认、PHP 7.1 到 8.x 全覆盖——这种成熟度,PHP 错误展示这个细分没什么对手。Symfony 自带的错误页也好看,但缺"一键跳 IDE"和"自定义数据表"这两个对调试效率影响最大的特性。我的判断是:任何 PHP 项目,开发环境不开 Whoops 就是给自己添堵。

最近浏览
IP用户:51.68.*.*
1 分钟前 MJ12 Bot
IP用户:66.249.*.*
21 分钟前 Googlebot
IP用户:61.150.*.*
57 分钟前 Firefox Windows 10
IP用户:43.163.*.*
1 小时前 Mobile Safari iOS 13.2
IP用户:209.85.*.*
2 小时前 Googlebot
IP用户:112.251.*.*
2 小时前 Firefox Windows 10
累计浏览次数:7
评论
点击登录
phpreturn,PHP武器库,专注PHP领域的项目和资讯,收录和介绍PHP相关项目。
最近浏览 点击登录
累计浏览次数:336551
一周浏览次数:2911
今日浏览次数:166

本站所有权利归 phpreturn.com 所有

举报/反馈/投稿邮箱:phpreturn@ulthon.com

鲁ICP备19027671号-2