whoops 错误页一键打开编辑器:PrettyPageHandler 的 setEditor 配置全指南

whoops 错误页一键打开编辑器:PrettyPageHandler 的 setEditor 配置全指南

【免费下载链接】whoops PHP errors for cool kids 【免费下载链接】whoops 项目地址: https://gitcode.com/gh_mirrors/wh/whoops

导读

本文讲解 whoops 的 pretty error page(可视化错误页)如何实现"在浏览器中点击出错文件路径,直接在你的 IDE/编辑器里打开对应文件与行号"这一能力。读完本文,你将掌握 setEditor() 的默认编辑器清单、%file/%line 占位符机制、自定义编辑器解析器(含 IntelliJ Platform 远程路径映射与 Ajax 方案),以及该功能在前端渲染与后端源码中的完整实现链路。本文依据仓库中的 docs/Open Files In An Editor.md 展开,并结合 PrettyPageHandler.php 源码、视图模板与单元测试进行纵深验证。

功能前提:本地文件访问

该特性属于 Whoops\Handler\PrettyPageHandler(可视化错误页处理器)的附属能力。它只在满足以下条件时可用:

  • 你使用的是 pretty error page 渲染的错误页(HTML 输出模式);
  • 你的 PHP 源文件对安装了编辑器的这台机器是本地可访问的。

也就是说,如果代码运行在远程服务器、容器或 CI 环境中,而编辑器安装在本机,需要先通过路径映射(如本文后面 IntelliJ 示例中的远程文件替换)把远程路径翻译成本机路径,链接才有意义。

最简用法:一行代码启用

使用一个字符串标识符即可指定目标编辑器:

<?php

use Whoops\Handler\PrettyPageHandler;

$handler = new PrettyPageHandler;
$handler->setEditor('sublime');

setEditor() 接受两种参数(源码见 PrettyPageHandler.php 的 setEditor 方法):

  1. 字符串标识符:必须是内置编辑器列表中的一个 key;
  2. 可调用对象(闭包):自定义解析器,返回编辑器 URL 字符串或数组。

若传入的字符串不在已知编辑器列表中,且不是可调用对象,setEditor() 会抛出 InvalidArgumentException,异常信息中会列出当前已知的所有编辑器标识符:

if (!is_callable($editor) && !isset($this->editors[$editor])) {
    throw new InvalidArgumentException(
        "Unknown editor identifier: $editor. Known editors:" .
        implode(",", array_keys($this->editors))
    );
}

方法返回 $this,支持链式调用。

默认支持的编辑器清单与 URL 协议

文档列出的默认支持编辑器如下(文档原文):

  • emacs - Emacs
  • idea - IDEA
  • macvim - MacVim
  • phpstorm - PhpStorm
  • sublime - Sublime Text 2 以及可能的 3(OS X 上可能需要专门的处理程序)
  • textmate - Textmate
  • xdebug - xdebug(使用 xdebug.file_link_format)
  • vscode - VSCode(通过 URL 打开 VS Code 的方式)
  • atom - Atom
  • espresso - Espresso
  • netbeans - Netbeans(基于 xdebug.file_link_format 的规则)

而源码中 $editors 属性(PrettyPageHandler.php 第 115-127 行)给出了每个标识符对应的真实 URL 模板,这也是理解整个机制的关键:

标识符常量实际 URL 模板
sublimeEDITOR_SUBLIMEsubl://open?url=file://%file&line=%line
textmateEDITOR_TEXTMATEtxmt://open?url=file://%file&line=%line
emacsEDITOR_EMACSemacs://open?url=file://%file&line=%line
macvimEDITOR_MACVIMmvim://open/?url=file://%file&line=%line
phpstormEDITOR_PHPSTORMphpstorm://open?file=%file&line=%line
ideaEDITOR_IDEAidea://open?file=%file&line=%line
vscodeEDITOR_VSCODEvscode://file/%file:%line
atomEDITOR_ATOMatom://core/open/file?filename=%file&line=%line
espressoEDITOR_ESPRESSOx-espresso://open?filepath=%file&lines=%line
netbeansEDITOR_NETBEANSnetbeans://open/?f=%file:%line
cursorEDITOR_CURSORcursor://file/%file:%line

说明:cursor(Cursor 编辑器)同样在源码的 $editors 列表中,并定义了 EDITOR_CURSOR 常量,但其未出现在原文档的内置清单里,属于仓库实现中已内置、文档未列举的扩展项。

关于 %file 与 %line 占位符

观察上面的模板可以发现,所有内置编辑器 URL 都包含 %file 与 %line 两个占位符。在真正生成链接时(getEditorHref 方法),whoops 会使用 rawurlencode() 对二者做 URL 编码后替换:

$editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']);
$editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']);

这意味着即使文件路径中包含空格、问号等特殊字符,也能被安全地编码进 URL。测试用例 PrettyPageHandlerTest.php 验证了这一行为:

$this->assertEquals(
    $handler->getEditorHref('/foo/with space?.php', 2324),
    'subl://open?url=file://%2Ffoo%2Fwith%20space%3F.php&line=2324'
);

xdebug 的特殊处理

xdebug 不是一个静态 URL 模板,而是一个动态解析器。在 PrettyPageHandler 的构造函数中(PrettyPageHandler.php 第 141-149 行)有这样的逻辑:

if (ini_get('xdebug.file_link_format') || get_cfg_var('xdebug.file_link_format')) {
    // Register editor using xdebug's file_link_format option.
    $this->editors['xdebug'] = function ($file, $line) {
        return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'));
    };

    // If xdebug is available, use it as default editor.
    $this->setEditor('xdebug');
}

即:只要 PHP 环境中配置了 xdebug.file_link_format,whoops 会自动把 xdebug 注册为默认编辑器。该格式串使用 %f(文件)和 %l(行号)占位,例如 subl://open?url=%f&line=%l。注意 xdebug 模式不做 URL 编码(与 %file/%line 的 rawurlencode 行为不同),这一点由测试 testEditorXdebug 显式验证:路径 /foo/with space?.php 会原样拼进 URL。

自定义编辑器:闭包解析器

内置清单之外,添加自己的编辑器非常简单。setEditor() 直接接收一个闭包,闭包接收 $file 和 $line 两个参数,返回一个 URL 字符串:

$handler->setEditor(function($file, $line) {
    return "whatever://open?file=$file&line=$line";
});

闭包支持两种返回值(解析逻辑见 getEditor 方法):

返回值 1:纯字符串 —— 直接被当作编辑器 URL 使用,链接以普通方式打开:

$handler->setEditor(function ($file, $line) {
    $file = rawurlencode($file);
    $line = rawurlencode($line);
    return "http://google.com/search/?q=$file:$line";
});

返回值 2:数组(url + ajax) —— 当编辑器协议会导致浏览器跳转离开错误页时,可以通过 ajax 标志改用 Ajax 请求,避免页面被替换:

$handler->setEditor(function ($file, $line) {
    return [
        'url'  => "http://my-editor-host/open?file=$file&line=$line",
        'ajax' => true,
    ];
});

数组中的 ajax 键默认为 false,只有显式设为 true 时才会走 Ajax 通道;getEditorAjax() 方法(PrettyPageHandler.php 第 542-553 行)负责返回这个标志,若解析结果中没有布尔型的 ajax 键则会抛出 UnexpectedValueException。测试 testSetEditorCallable 覆盖了字符串返回、数组返回(ajax 为 true/false)以及返回 false(表示该帧不生成编辑器链接)四种情形。

实战示例:IntelliJ Platform(PhpStorm Opener)

原文档给出了一个完整的 IntelliJ Platform 集成示例,它同时用到了远程路径映射和 Ajax 请求两个关键技巧:

$handler->setEditor(
    function ($file, $line) {
        // 如果开发服务器不在本地,最好把远程文件映射到本地
        $translations = array('^' . __DIR__ => '~/Development/PhpStormOpener'); // 改成你的路径

        foreach ($translations as $from => $to) {
            $file = preg_replace('#' . $from . '#', $to, $file, 1);
        }

        // IntelliJ Platform 要求发送 Ajax 请求,否则浏览器会退出当前页面
        return array(
            'url' => "http://localhost:63342/api/file/?file=$file&line=$line",
            'ajax' => true
        );
    }
);

其中 $translations 数组通过正则前缀替换,把服务器上的源文件路径(如 /var/www/project)翻译成本机开发目录(如 ~/Development/PhpStormOpener),从而突破"源文件必须本机可访问"的前提限制;ajax => true 则让 whoops 前端用 Ajax 触发 IntelliJ 的 REST 接口,而不是整页跳转。

前端如何消费 ajax 标志

错误页模板 frame_code.html.php 中,每个调用栈帧的文件名都会尝试渲染成"Open: 文件路径"链接:

<?php if ($filePath && $editorHref = $handler->getEditorHref($filePath, (int) $line)): ?>
  <a href="<?php echo $editorHref ?>" class="editor-link"<?php echo ($handler->getEditorAjax($filePath, (int) $line) ? ' data-ajax' : '') ?>>
    Open:
    <strong><?php echo $tpl->breakOnDelimiter('/', $tpl->escape($filePath ?: '<#unknown>')) ?></strong>
  </a>
<?php else: ?>
  <strong><?php echo $tpl->breakOnDelimiter('/', $tpl->escape($filePath ?: '<#unknown>')) ?></strong>
<?php endif ?>

当 getEditorHref() 返回 false(例如自定义解析器返回了 false,或该帧没有文件)时,文件名退化为纯文本、不生成链接。

而 data-ajax 标记由前端脚本消费:whoops.base.js 中,所有带 data-ajax 的 .editor-link 点击后不会跳转页面,而是用 $.get(this.href) 发起 Ajax 请求:

// Avoid to quit the page with some protocol (e.g. IntelliJ Platform REST API)
$ajaxEditors.on('click', function(e){
  e.preventDefault();
  $.get(this.href);
});

注册命名编辑器:addEditor()

如果某个自定义解析器要被多个项目复用,或者在同一个 handler 里维护多套编辑器方案,可以使用 addEditor($identifier, $resolver) 先注册再引用(PrettyPageHandler.php 第 464-468 行):

$handler->addEditor('my-editor', function ($file, $line) {
    return "myeditor://open?file=$file&line=$line";
});

$handler->setEditor('my-editor');

addEditor 的 resolver 同样可以是字符串模板(含 %file/%line 占位符)或闭包。注册后即可像内置编辑器一样通过字符串标识符调用。测试 testAddEditor 展示了注册、引用、取链接的完整流程:

$handler->addEditor('test-editor', function ($file, $line) {
    return "cool beans $file:$line";
});
$handler->setEditor('test-editor');
// getEditorHref('hello', 20) === 'cool beans hello:20'

需要注意:addEditor 注册的解析器若与 setEditor 传入的标识符同名,getEditor() 在解析时优先使用闭包解析器,而非把标识符当作静态字符串(见 getEditor 方法 的分支判断)。

整体工作链路

结合以上内容,可以把"错误页打开编辑器"的完整链路总结如下:

  1. 配置阶段:new PrettyPageHandler(构造函数探测 xdebug.file_link_format,存在则自动设为默认编辑器)→ setEditor('sublime') 或 setEditor(闭包) / addEditor(...);
  2. 渲染阶段:错误发生时,handle() 收集异常帧,模板 frame_code.html.php 对每个帧调用 getEditorHref($file, $line) 生成 URL,并用 getEditorAjax() 判断是否附加 data-ajax;
  3. 占位符替换:getEditorHref() 内部对 %file、%line 做 rawurlencode 替换(xdebug 模式例外,不做编码);
  4. 点击触发:普通链接直接触发系统协议打开编辑器;带 data-ajax 的链接由 whoops.base.js 拦截,改用 Ajax 请求,避免浏览器离开错误页。

该能力的对应文档入口同时收录于项目 README.md 的功能特性列表,可直接作为后续查阅的起点。

常见问题与排查要点

  • 点击"Open"没反应:优先确认 PHP 源文件路径在编辑器所在机器上真实存在;远程环境需像 IntelliJ 示例那样做路径映射。
  • 抛 InvalidArgumentException: Unknown editor identifier:setEditor() 传入的字符串不在 $editors 键列表中,检查拼写,或改用 addEditor() 先注册。
  • 自定义闭包返回了非字符串、非数组:getEditorHref() 会抛 UnexpectedValueException,提示解析结果必须是字符串或合法的编辑器数组。
  • 浏览器被协议跳转"顶掉"错误页:改用 ['url' => ..., 'ajax' => true] 数组返回形式。
  • xdebug 模式下路径未编码:这是有意为之(与 xdebug 原生行为保持一致),若路径含特殊字符,建议在 xdebug.file_link_format 中自行处理。

以上所有行为均有仓库源码与单元测试(PrettyPageHandlerTest.php)作为依据,读者可以在本地 phpunit.xml.dist 配置的测试环境中运行对应用例进一步验证。

【免费下载链接】whoops PHP errors for cool kids 【免费下载链接】whoops 项目地址: https://gitcode.com/gh_mirrors/wh/whoops

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值