whoops 错误页一键打开编辑器:PrettyPageHandler 的 setEditor 配置全指南
【免费下载链接】whoops PHP errors for cool kids 项目地址: 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 方法):
- 字符串标识符:必须是内置编辑器列表中的一个 key;
- 可调用对象(闭包):自定义解析器,返回编辑器 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- Emacsidea- IDEAmacvim- MacVimphpstorm- PhpStormsublime- Sublime Text 2 以及可能的 3(OS X 上可能需要专门的处理程序)textmate- Textmatexdebug- xdebug(使用xdebug.file_link_format)vscode- VSCode(通过 URL 打开 VS Code 的方式)atom- Atomespresso- Espressonetbeans- Netbeans(基于xdebug.file_link_format的规则)
而源码中 $editors 属性(PrettyPageHandler.php 第 115-127 行)给出了每个标识符对应的真实 URL 模板,这也是理解整个机制的关键:
| 标识符 | 常量 | 实际 URL 模板 |
|---|---|---|
sublime | EDITOR_SUBLIME | subl://open?url=file://%file&line=%line |
textmate | EDITOR_TEXTMATE | txmt://open?url=file://%file&line=%line |
emacs | EDITOR_EMACS | emacs://open?url=file://%file&line=%line |
macvim | EDITOR_MACVIM | mvim://open/?url=file://%file&line=%line |
phpstorm | EDITOR_PHPSTORM | phpstorm://open?file=%file&line=%line |
idea | EDITOR_IDEA | idea://open?file=%file&line=%line |
vscode | EDITOR_VSCODE | vscode://file/%file:%line |
atom | EDITOR_ATOM | atom://core/open/file?filename=%file&line=%line |
espresso | EDITOR_ESPRESSO | x-espresso://open?filepath=%file&lines=%line |
netbeans | EDITOR_NETBEANS | netbeans://open/?f=%file:%line |
cursor | EDITOR_CURSOR | cursor://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 方法 的分支判断)。
整体工作链路
结合以上内容,可以把"错误页打开编辑器"的完整链路总结如下:
- 配置阶段:
new PrettyPageHandler(构造函数探测xdebug.file_link_format,存在则自动设为默认编辑器)→setEditor('sublime')或setEditor(闭包)/addEditor(...); - 渲染阶段:错误发生时,
handle()收集异常帧,模板 frame_code.html.php 对每个帧调用getEditorHref($file, $line)生成 URL,并用getEditorAjax()判断是否附加data-ajax; - 占位符替换:
getEditorHref()内部对%file、%line做rawurlencode替换(xdebug 模式例外,不做编码); - 点击触发:普通链接直接触发系统协议打开编辑器;带
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 项目地址: https://gitcode.com/gh_mirrors/wh/whoops
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



