Skip to content

feat(di): 代理类错误可直接定位原始源码(非代理文件) - #7795

Open
tw2066 wants to merge 1 commit into
hyperf:3.2from
tw2066:3.2-di-file
Open

feat(di): 代理类错误可直接定位原始源码(非代理文件)#7795
tw2066 wants to merge 1 commit into
hyperf:3.2from
tw2066:3.2-di-file

Conversation

@tw2066

@tw2066 tw2066 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

代理类异常行号修正(Proxy Line Map)改动说明

版本基线

  • Hyperf 3.2
  • PHP >= 8.2
  • nikic/php-parser ^5.6(验证版本 5.8.0)

Hyperf 3.2 使用 php-parser v5 的 ParserFactory::createForNewestSupportedVersion()ClosureUse 节点。line map 缓存版本提升为 3,升级后不会复用 Hyperf 3.1/php-parser v4 生成的代理映射。

一、背景

Hyperf DI 使用 AST 重新生成代理类。重新打印、trait 注入和 AOP 方法改写都会改变代理文件行号,因此异常位置可能指向 runtime/container/proxy/*.proxy.php,无法直接定位原始源码。

本方案在生成代理文件时同时生成 line-map.php,记录代理文件与原始文件的行号区间。运行时由异常 formatter 在生成日志或错误输出时应用映射,不修改异常对象。

二、设计原则

  1. 不包装普通方法:未被 AOP 改写的方法保持原有控制流,不生成方法级 try/catch
  2. 展示层统一处理DefaultFormatter 在生成异常文本时统一应用映射,代理方法和 dispatcher 都不捕获或改写异常。
  3. 映射失败时保持原状:找不到可靠映射时继续显示代理文件位置,不猜测业务源码位置。
  4. 生成产物可失效:map 文件包含格式版本。升级格式、首次启用或 map 条目缺失时会重新生成代理与映射。
  5. 组件依赖保持可选hyperf/exception-handler 不强依赖 hyperf/di,formatter 仅在 LineMapFixer 存在时调用。

三、运行流程

生成阶段

ScanConfig::getLineMap() -> Scanner -> ProxyManager -> Ast::buildLineMap() -> line-map.php

line-map.php 格式:

return [
    'version' => 3,
    'maps' => [
        App\Service\FooService::class => [
            'file' => '/app/app/Service/FooService.php',
            'ranges' => [
                // proxyStart, proxyEnd, originStart, originEnd
                [30, 30, 18, 18],
            ],
            'methods' => [
                'handle' => [25, 40, 13, 28],
            ],
        ],
    ],
];

映射只收集目标 class/trait/enum 的方法,避免同一文件中其他类的同名方法覆盖结果。语句精确映射要求节点类型和顶层表达式类型一致;遇到无法识别的 Visitor 结构变化时停止该方法的后续语句对齐。运行时只使用可靠的语句级映射,不根据方法范围猜测源码位置。

展示阶段

Throwable -> DefaultFormatter -> LineMapFixer::format() -> mapped log text

因此代理类方法和 ProxyTrait::__proxyCall() 都不会为了行号映射生成以下代码:

catch (\Throwable $__hyperf_exception__) {
    LineMapFixer::fix($__hyperf_exception__, __CLASS__);
    throw $__hyperf_exception__;
}

四、配置

config/autoload/annotations.php

return [
    'scan' => [
        'paths' => [...],
        'line_map' => true,
    ],
];

line_map 默认值为 true

行为
true 生成 line-map.php,异常 formatter 输出原始源码位置
false 不生成映射并删除残留 map;异常继续显示代理文件位置

首次启用、map 格式升级或某个代理缺少 map 条目时,ProxyManager 会强制重新生成相关代理,避免复用不兼容缓存。旧版本遗留的 __hyperf_exception__ 方法包装在关闭功能时也会触发一次重新生成。

五、边界

  1. PHP 不提供安全修改现有 Throwable file/line/trace 的 API。$e->getFile()$e->getLine()$e->getTrace() 始终返回真实的代理执行位置。
  2. 框架内置日志路径应使用 FormatterInterface。业务自定义异常 handler 如果直接拼接 $e->getFile(),需要改为注入 FormatterInterface 并调用 format($e),或显式调用 LineMapFixer::resolveLocation()
  3. Hyperf 的 Whoops handler 使用只读 Inspector 副本映射 HTML、JSON、XML 和纯文本错误页。其他直接读取 Throwable 的第三方展示器需要单独接入 source map。
  4. 宽区间换算结果会限制在原始区间内,避免映射到原方法范围之外。
  5. line-map.php 是运行时生成产物,不应提交到版本库。

六、并发与缓存

map 写入使用每进程唯一临时文件、文件锁和同目录 rename(),避免多个进程共享固定 .tmp 文件。加载缓存按代理目录隔离,支持同一进程读取不同代理目录。

七、测试

覆盖范围包括:

  • 普通代理方法不生成 catch 的既有 AST 快照;
  • 多 class 文件只收集目标类方法;
  • 行号换算不会超过原始区间;
  • formatter 能读取 version 3 map 并修正输出,同时不修改 Throwable;
  • Whoops Inspector 能修正错误页 frame,同时不修改 Throwable;
  • AOP visitor 和既有 exception handler 行为回归。

- 在Ast类中新增buildLineMap方法用于构建代理代码与原始代码的行号映射关系
- 新增LineMapFixer类用于将代理文件中的异常位置转换回原始源码位置
- 在ProxyManager中集成行号映射生成功能并添加line-map.php缓存文件
- 更新扫描配置支持line_map选项控制是否启用行号映射
- 在异常处理器中集成行号映射功能,使异常堆栈显示原始代码位置
- 添加LineMapInspector和LineMapInspectorFactory用于Whoops异常显示的行号转换
- 新增相关测试验证行号映射功能的正确性和异常处理流程
@tw2066 tw2066 changed the title feat(di): 添加代理代码行号映射功能以支持异常位置转换 feat(di): 代理类错误可直接定位原始源码(非代理文件) Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant