aaron-dev / xhprof-webman
aaron-dev/xhprof-webman is a code performance analysis plugin for webman, Laravel, ThinkPHP, Hyperf, Yii2, Yii3, Symfony, Slim 4, WordPress, Joomla and Drupal. Uses the xhprof extension for profiling, Redis for storage, and renders browser-based performance reports.
Requires
- php: >=8.0
- ext-redis: *
- ext-xhprof: *
Requires (Dev)
- phpstan/phpstan: 2.2.16
- phpunit/phpunit: ^11.5
Suggests
- drupal/core: ^10.0|^11.0
- hyperf/framework: ^3.0
- joomla/event: ^3.0
- laravel/framework: ^9.0|^10.0|^11.0
- roots/wordpress: >=6.4
- slim/slim: ^4.12
- symfony/http-kernel: ^6.4|^7.0
- topthink/framework: ^6.0|^8.0
- webman/redis: ^2.1
- workerman/webman: ^2.1
- yiisoft/middleware-dispatcher: ^5.0
- yiisoft/yii2: ^2.0
Provides
None
Conflicts
None
Replaces
None
README
中文 · English · 한국어 · Русский · Deutsch · Français · Español · Português · العربية · हिन्दी · বাংলা · Bahasa Indonesia · 日本語
兼容 webman / Laravel / ThinkPHP / Hyperf / Yii2 / Yii3 / Symfony / Slim 4 / WordPress / Joomla / Drupal / 原生 PHP(无框架)的代码性能分析插件。
基于 xhprof 扩展采集数据并存入 Redis,开发者可通过浏览器快速访问性能分析报告,排查代码性能瓶颈。
同一只小火苗也是报告页的站点图标、左上角品牌图标与表格排序图标(src/html/pet.svg、src/html/images/sort_*.svg,随 assets_url 前缀服务)。
请求记录
单次运行报告
对比两次运行 — 在「请求记录」列表里勾选恰好两条(每行一个复选框,表头可全选),点「对比选中」进入 diff 视图。两侧按时间取先后(run1 = 较早、run2 = 较晚,与列表当前排序无关),着色语义是「从 run1 到 run2」的改善 / 回归,页内「反转」链接可随时交换两侧。
导出于机器消费 — 报告页动作栏提供 JSON / CSV 导出(单次运行、对比、聚合三种视图都可导);?format=json 不带 run 参数时返回运行列表 JSON(每条含 run_id、请求元数据与列表口径的头部信息),巡检脚本 / 看板取 run_id 不必再解析 HTML。带 symbol= 的导出请求会返回 400——导出没有单函数视图,静默给一份全量表比报错更坏。
环境要求
- PHP >= 8.0
- xhprof 扩展
- redis 扩展
- Redis 服务
兼容框架与最低版本
| 框架 | 最低版本 | 最低 PHP | 入口类 | 挂载方式 |
|---|---|---|---|---|
| webman | workerman/webman ^2.1 |
8.0 | Webman\XhprofMiddleware |
config/middleware.php 注册全局中间件 |
| Laravel | laravel/framework ^9.0|^10.0|^11.0|^12.0|^13.0 |
8.0 | Laravel\Middleware |
11+ 用 bootstrap/app.php 的 ->withMiddleware() 追加;10 及以下在 app/Http/Kernel.php 注册全局中间件 |
| ThinkPHP | topthink/framework ^6.0|^8.0 |
8.0 | Thinkphp\Middleware |
app/middleware.php 注册全局中间件 |
| Hyperf | hyperf/framework ^3.0 |
8.0 | Hyperf\Middleware |
ConfigProvider 自动注册 |
| Yii3 | yiisoft/middleware-dispatcher ^5.0 |
8.1 | Yii3\XhprofMiddleware |
config/web/di/application.php 注册,须放中间件队列第一位 |
| Symfony | symfony/http-kernel ^6.4|^7.0|^8.0 |
8.1(6.4)/ 8.2(7.x)/ 8.4(8.x) | Symfony\XhprofListener |
config/services.yaml 加 kernel.event_subscriber tag |
| Slim 4 | slim/slim ^4.12 |
8.0 | Slim\XhprofMiddleware |
$app->add(...),须最后 add |
| WordPress | 6.4+ | 8.0 | Wordpress\XhprofPlugin |
复制到 wp-content/mu-plugins/ |
| Joomla | 4.4 / 5.x | 8.1 | Joomla\Extension\Xhprof |
复制到 plugins/system/,后台「发现」安装 |
| Drupal | 10.x / 11.x | 8.1(10.x)/ 8.3(11.x) | xhprof 模块(Drupal\XhprofMiddleware) |
标准模块,启用即可 |
| 原生 PHP(无框架) | —(无外部包) | 8.0 | Native\XhprofBootstrap |
入口文件顶部一行 XhprofBootstrap::start(),无需注册控制器与路由 |
| Yii2 | yiisoft/yii2 ^2.0 |
8.0 | Yii2\XhprofBootstrap |
config/web.php 的 bootstrap 数组注册,无需注册控制器与路由 |
入口类命名空间前缀统一为 ErikWang2013\Xhprof\(上表省略)。十二家都不需要你注册控制器与路由:报告页与静态资源由入口类自行接管,Drupal 则由模块路由提供(见 Drupal 一节)。
本包声明 php >= 8.0,但 Yii3 依赖的 yiisoft/* 组件要求 PHP 8.1+,所以 Yii3 在 PHP 8.0 上不可用;Symfony 7.x、Drupal 11.x 同理需要更高的 PHP 版本。逐步接入方式见下方「框架配置」。
安装
xhprof 扩展从 PECL 安装(PHP 8 下当前是 2.3.x):
pecl install xhprof
php.ini 中增加 xhprof 配置:
[xhprof] extension=xhprof.so xhprof.output_dir=/tmp/xhprof
Composer 安装:
composer require aaron-dev/xhprof-webman
快速开始
三步走完最短路径:
- 装扩展 ——
pecl install xhprof,并在 php.ini 里加上[xhprof]段(extension=xhprof.so、xhprof.output_dir=/tmp/xhprof)。 - 起 Redis ——
redis-server --daemonize yes;或用你已有的实例(连接参数落在各框架配置文件config/xhprof.php的redis子数组里)。 - 接入并打开报告页 ——
composer require aaron-dev/xhprof-webman,在任一家框架按「框架配置」把入口类挂上,产生一次业务请求后访问http://<你的站点>/xhprof。
不想装环境?
demo/里有一套 docker compose 演示(原生 PHP 入口,不依赖任何框架):cd demo && docker compose up -d,然后打开http://127.0.0.1:8080/xhprof就能看到一份真实报告页;说明见demo/README.md。
排障速查
| 症状 | 先查什么 |
|---|---|
| 报告页空白,列表里没有记录 | enable 是否为 true;sample_rate 是否被调成 0(此时只有带 X-Xhprof-Token 头的请求会被采样);Redis 里 <key_prefix>:run_id 是否为空 |
| 报告页返回 403 / 401 | 403:ip_allowlist 挡住了当前 IP(或请求 IP 来自转发头而 trusted_proxies 为空),或配了 auth_token 而 URL 没带 ?token=;401 并弹出浏览器凭据框:配了 auth_basic 且输入的用户名/密码与配置不符 |
| 报错连不上 Redis | redis 扩展是否装上(php -m 输出里有 redis)、Redis 是否在运行、redis 子数组的 host / port / password / database 是否与实例一致 |
| 装了扩展,业务请求却不落库 | 入口类是否真的挂上(见「框架配置」);请求路径是否命中 ignore_url_arr;max_runs_per_minute 是否已达上限(超出即不采,要等下一分钟) |
| 报告页能打开,样式/脚本却 404 | assets_url 前缀是否与部署路径一致;反向代理是否把该前缀也转发到应用 |
框架配置
Webman
1. 注册全局中间件 — config/middleware.php:
return [ '' => [ ErikWang2013\Xhprof\Webman\XhprofMiddleware::class, ], ];
2. 报告页与静态资源 — 无需注册控制器与路由:中间件在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页并返回,命中资源路径(前缀从配置项 assets_url 读,默认 /xhprof-assets)直接输出静态资源。
3. 配置 — 见 config/plugin/aaron-dev/xhprof/xhprof.php。
Laravel
1. 注册中间件 — Laravel 11 及以上(slim skeleton 起就没有 app/Http/Kernel.php)在 bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) { $middleware->append(\ErikWang2013\Xhprof\Laravel\Middleware::class); })
Laravel 10 及以下仍在 app/Http/Kernel.php 的 protected $middleware 数组里追加 \ErikWang2013\Xhprof\Laravel\Middleware::class。
2. 报告页与静态资源 — 无需注册控制器与路由:中间件在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页并返回,命中资源路径(前缀从配置项 assets_url 读,默认 /xhprof-assets)直接输出静态资源。
3. 发布配置:
php artisan vendor:publish --tag=xhprof-config
配置文件在 config/xhprof.php。Laravel 支持自动发现 ServiceProvider。
4. CLI 与队列(需要时先打开 sample_cli,默认关闭)— 两类没有 HTTP 请求的入口都在采样窗口内跑:
- 队列 worker:在
AppServiceProvider::boot()里把四个事件接到包内监听器,按消息开停(常驻 worker 不漏):
use ErikWang2013\Xhprof\Laravel\XhprofQueueListener; use Illuminate\Queue\Events\{JobProcessing, JobProcessed, JobFailed, JobExceptionOccurred}; Event::listen(JobProcessing::class, [XhprofQueueListener::class, 'onJobProcessing']); Event::listen(JobProcessed::class, [XhprofQueueListener::class, 'onJobProcessed']); Event::listen(JobFailed::class, [XhprofQueueListener::class, 'onJobFailed']); Event::listen(JobExceptionOccurred::class, [XhprofQueueListener::class, 'onJobExceptionOccurred']);
- artisan 命令:
xhprof:profile随包自动注册(Laravel 自动发现 ServiceProvider),直接用:
php artisan xhprof:profile "migrate --force"
- 自定义脚本 / 定时任务:非 artisan 入口(自写 PHP 脚本、闭包任务)在任务体外面包一层:
\ErikWang2013\Xhprof\Laravel\XhprofCli::start(); try { /* 原逻辑 */ } finally { \ErikWang2013\Xhprof\Laravel\XhprofCli::stop(); }
窗口按任务开停(同步派发的子任务嵌套时只记最外层一条),落库的 request_uri 记为 cli:<脚本名>。
ThinkPHP
1. 注册中间件 — app/middleware.php:
return [ \ErikWang2013\Xhprof\Thinkphp\Middleware::class, ];
2. 报告页与静态资源 — 无需注册控制器与路由:中间件在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页并返回,命中资源路径(前缀从配置项 assets_url 读,默认 /xhprof-assets)直接输出静态资源。
3. 配置 — 复制 vendor/aaron-dev/xhprof-webman/src/Thinkphp/config/xhprof.php 到项目 config/xhprof.php。
Hyperf
1. 中间件自动注册 — ConfigProvider 自动将中间件加入 HTTP 中间件队列。
2. 报告页与静态资源 — 无需注册控制器与路由:中间件在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页并返回,命中资源路径(前缀从配置项 assets_url 读,默认 /xhprof-assets)直接输出静态资源。
3. 发布配置:
php bin/hyperf.php vendor:publish aaron-dev/xhprof-webman
配置输出在 config/autoload/xhprof.php。
Yii3
1. 注册中间件 — config/web/di/application.php:
use ErikWang2013\Xhprof\Yii3\XhprofMiddleware; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; return [ MiddlewareDispatcher::class => [ 'class' => MiddlewareDispatcher::class, // the first element is the outermost middleware (runs first, finishes last) 'withMiddlewares()' => [[ XhprofMiddleware::class, // ... other middlewares ]], ], ];
两个容易写错的地方(均已实测):
- 不要写
'__construct()' => ['middlewares' => [...]]:MiddlewareDispatcher::__construct()只接受MiddlewareFactory与可选的EventDispatcherInterface,没有middlewares参数,中间件列表只能通过withMiddlewares()这个实例方法注入。 - 不要把实例(
new XhprofMiddleware(...))放进withMiddlewares():定义只接受类名字符串 / 数组定义 / callable。传实例时注册阶段不报错,直到dispatch()才抛TypeError(MiddlewareFactory::create()的形参类型是callable|array|string)。
2. 报告页与静态资源 — 无需注册控制器与路由:XhprofMiddleware 是 PSR-15 中间件,在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页并返回,命中资源路径(前缀默认 /xhprof-assets)直接输出静态资源。报告页响应由入口类显式带上 Content-Type: text/html; charset=UTF-8:PSR-7 响应没有默认值、Yii3 的响应发送器也不补,缺了它浏览器会把 HTML 报告按纯文本渲染。
3. 配置 — 默认值在包内 src/Yii3/config/xhprof.php,字段含义见「配置项说明」。需要覆盖时在 DI 里注入 $config:
XhprofMiddleware::class => [
'class' => XhprofMiddleware::class,
'__construct()' => [
'config' => [
'enable' => true,
'auth_token' => 'your-token',
'redis' => [
'host' => '127.0.0.1',
'port' => 6379,
'password' => '',
'database' => 0,
'timeout' => 1.0,
],
],
],
],
redis 子数组是 Yii3 独有的:不注入 CacheInterface 时,中间件用它直连 phpredis。assets_url 可以改成任意前缀:报告页的 CSS/JS 链接与 StaticController 读的是同一个配置项(默认 /xhprof-assets)。目录/子路径部署下的剩余限制见验证与已知限制。
4. 版本要求 — Yii3 依赖的 yiisoft/* 组件要求 PHP >= 8.1,本包虽然声明 php >= 8.0,但在 PHP 8.0 上无法使用 Yii3 接入。
Symfony
1. 注册事件订阅器 — config/services.yaml:
services: ErikWang2013\Xhprof\Symfony\XhprofListener: tags: - { name: kernel.event_subscriber }
2. 报告页与静态资源 — 无需注册控制器与路由:监听器在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页并返回,命中资源路径(前缀默认 /xhprof-assets)直接输出静态资源。
3. 配置 — 默认值在包内 src/Symfony/config/xhprof.php,字段含义见「配置项说明」。
4. 子请求与异常兜底 — 监听 kernel.request(优先级 10000)与 kernel.response(优先级 -10000)。用 isMainRequest() 过滤 ESI/fragment 子请求,否则子请求结束会把采样提前 stop;请求开始时另注册幂等的 register_shutdown_function 兜底——HttpKernel 重抛异常时 kernel.response 不会触发,没有兜底则采样状态会泄漏到下一个请求。
Slim 4
1. 注册中间件 — public/index.php:
use ErikWang2013\Xhprof\Slim\XhprofMiddleware; $app->addRoutingMiddleware(); // must be added last: Slim's middleware stack is LIFO, added later = further out = runs first $app->add(new XhprofMiddleware( $app->getResponseFactory() ));
后三个构造参数都是可选的,省略即用包内默认值:
- 第 2 个参数
array $config:用户配置数组,与包内src/Slim/config/xhprof.php用array_replace合并(整块替换,不会递归合并ignore_url_arr这类列表键)。 - 第 3 个参数
CacheInterface $cache:省略则惰性new \Redis()(构造函数刻意不碰 ext-redis,没装扩展也不会在建适配器时就炸);要注入自己的连接就传new \ErikWang2013\Xhprof\Slim\Adapter\RedisAdapter($redis),或任何实现了ErikWang2013\Xhprof\Core\Contract\CacheInterface的对象。 - 第 4 个参数
LoggerInterface $logger:省略即new \ErikWang2013\Xhprof\Slim\Adapter\LogAdapter(),日志会被静默丢弃(Slim 自身不提供 PSR-3 logger);要记录就传new LogAdapter($psrLogger),$psrLogger是你自己已有的 PSR-3 logger。
不要写 $app->add(XhprofMiddleware::class):类名字符串会由 Slim 的 CallableResolver 解析成 new XhprofMiddleware($container)——只传容器这一个参数,而且解析推迟到请求期。结果是 add() 阶段不报错、第一个请求才抛 TypeError,属于最难排查的失败方式。一律用上面的显式 new。
2. 报告页与静态资源 — 无需注册控制器与路由:中间件在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页并返回,命中资源路径(前缀默认 /xhprof-assets)直接输出静态资源。
3. 配置 — 默认值在包内 src/Slim/config/xhprof.php,字段含义见「配置项说明」。
4. 挂载顺序 — Slim 的中间件栈是 LIFO(实测两个中间件的执行序列为 B:before → A:before → A:after → B:after):add() 越晚越靠外层、越先执行。所以 xhprof 必须最后 add,并且晚于 addRoutingMiddleware()——否则 /xhprof 不在路由表里,RoutingMiddleware 会先抛 HttpNotFoundException,请求根本到不了中间件。报告页响应由入口类显式带上 Content-Type: text/html; charset=UTF-8:PSR-7 响应没有默认值、Slim 的 ResponseEmitter 也不补,缺了它浏览器会把 HTML 报告按纯文本渲染。
WordPress
1. 安装 mu-plugin — 把包内引导文件复制到 wp-content/mu-plugins/:
cp vendor/aaron-dev/xhprof-webman/wordpress/xhprof-webman.php wp-content/mu-plugins/
wordpress/xhprof-webman.php 带 plugin header,引导 Wordpress\XhprofPlugin。mu-plugin 会被自动加载,不需要在后台启用。
2. 报告页与静态资源 — 无需注册控制器与路由:入口类在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页并返回,命中资源路径(前缀默认 /xhprof-assets)直接输出静态资源。
3. 配置 — 默认值在包内 src/Wordpress/config/xhprof.php,字段含义见「配置项说明」。用 ignore_url_arr 排除 wp-cron.php、admin-ajax.php 等高频路径。
需要覆盖配置(Redis 地址、auth_token 等)时,在 wp-config.php 里定义常量即可(mu-plugin 加载很早,常量此时已可用):
define('XHPROF_WEBMAN_CONFIG', [ 'auth_token' => 'your-token', 'redis' => ['host' => '127.0.0.1', 'port' => 6379, 'password' => '', 'database' => 0], ]);
也可以挂 xhprof_webman_config 过滤器(在主题或插件里):它在常量之上叠加,返回值与上面数组同形。
4. 采样窗口的结构性限制 — 采样窗口是 plugins_loaded → shutdown,不包含 wp-settings.php 的引导与插件加载本身。这是 WordPress 的结构性限制,该阶段的工作量无法被采样。
Joomla
1. 安装插件 — 把包内 joomla/ 目录复制到站点的 plugins/system/xhprof/:
cp -r vendor/aaron-dev/xhprof-webman/joomla/ plugins/system/xhprof/
包内 joomla/ 就是插件本体:xhprof.xml 清单、services/provider.php、src/Extension/Xhprof.php。入口类是 ErikWang2013\Xhprof\Joomla\Extension\Xhprof(CMSPlugin)。复制后在后台「系统 → 管理 → 扩展 → 发现」里执行「发现」并安装启用。
2. 报告页与静态资源 — 无需注册控制器与路由:插件在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页并返回,命中资源路径(前缀默认 /xhprof-assets)直接输出静态资源。
3. 配置 — 默认值在包内 src/Joomla/config/xhprof.php,字段含义见「配置项说明」。已知取舍:这里读的是包内配置文件,而不是插件参数——插件参数要读数据库,而配置读取发生在每个请求上。
4. 采样边界 — 采样窗口是 ApplicationEvents::AFTER_INITIALISE → ApplicationEvents::AFTER_RESPOND;AFTER_INITIALISE 时另注册幂等 register_shutdown_function 兜底,异常路径下 AFTER_RESPOND 不保证送达,没有兜底采样状态会泄漏到下一个请求。
Drupal
1. 启用模块 — 包内 drupal/xhprof/ 是标准 Drupal 模块(xhprof.info.yml / xhprof.routing.yml / xhprof.services.yml),放到站点的 modules/custom/xhprof/ 后在后台「扩展」页勾选启用(或 drush en xhprof)。
2. 报告页与静态资源 — Drupal 是十二个框架里唯一走「模块 + 路由」这一形态的:xhprof.routing.yml 注册报告路径 /xhprof 与资源路径 /xhprof-assets,默认由模块 Controller 输出;其余十一家的入口类在采样开始前自行短路,自服务报告页与静态资源,不注册路由。改成自定义 assets_url 前缀时资源改由中间件接管:模块的资源路由 path 写死在 xhprof.routing.yml(/xhprof-assets/{file}),匹配不到别的前缀。
3. 配置 — 配置是模块内的 typed config:默认值在 drupal/xhprof/config/install/xhprof.settings.yml,结构定义在 drupal/xhprof/config/schema/xhprof.schema.yml。字段含义见「配置项说明」。
4. 中间件注册 — 在模块的 xhprof.services.yml 里注册中间件服务:
services: xhprof.http_middleware: class: ErikWang2013\Xhprof\Drupal\XhprofMiddleware arguments: ['@config.factory', '@logger.factory'] tags: - { name: http_middleware, priority: 1000 }
内侧 kernel 由 Drupal 的 StackedKernelPass 自动前插为构造参数 0,不要自己写:写了会得到两个内侧 kernel,Drupal <= 11.2.x(含整个 10.x)在容器编译期直接失败,11.3.0 及以上则在每个请求上抛 TypeError。采样在 finally 中结束。
5. 注意事项
priority: 1000让中间件位于页面缓存之外(core 现有最高是 negotiation:D10 为 400、D11 为 500,而页面缓存是 200),因此命中 Drupal 页面缓存的请求也会被采样。对性能分析工具来说这是期望行为,但使用者需要知道。- 缓存默认开箱即用:中间件内部默认用本包自带的 Redis 适配器(本包硬依赖 ext-redis),也可以按上面的
arguments形式传入一个可选的CacheInterface覆写。若缓存不可用,落库失败会被XhprofProfiler::stop()吞成一条日志,不会报错。 - 报告页
/xhprof与/xhprof-assets/*的请求不采样:中间件在xhprofStart()之前按路径跳过采样。响应仍由xhprof.routing.yml的 Controller 产生(不是短路)。因此即便把ignore_url_arr设为[](什么都不过滤),这两个请求也不会出现在报告里。
原生 PHP(无框架)
适用于没有框架、只有一个前端控制器的应用(public/index.php 之类)。
1. 在入口文件顶部加一行:
\ErikWang2013\Xhprof\Native\XhprofBootstrap::start();
要改配置就把数组传进这一行(键集与另外十一家一致,默认值在 src/Native/config/xhprof.php):
\ErikWang2013\Xhprof\Native\XhprofBootstrap::start([ 'enable' => true, 'auth_token' => 'xxx', ]);
第 2、3 个参数是可选注入点:CacheInterface $cache 与 LoggerInterface $logger(默认分别是本包的 Redis 适配器与 error_log)。返回值是本次请求的入口实例(stop() 幂等),同一进程里要提前停表就调它。
2. 报告页与静态资源 — 无需注册控制器与路由:这一行在采样开始前判断请求路径,命中报告路径 /xhprof 直接输出报告页(带 Content-Type: text/html; charset=UTF-8 与 Cache-Control: no-cache, private,auth_token 照常生效),命中资源路径(前缀从配置项 assets_url 读,默认 /xhprof-assets)直接输出静态资源。代价写在明处:这两条路径处理完就 exit——本次请求的余下流程(这一行之后的路由、容器引导、会话启动,以及应用自己注册的请求收尾逻辑)不会执行。
3. 采样窗口 = 这一行 → 进程 shutdown(挂的是 register_shutdown_function)。边界照实说:不包含这一行之前的代码(composer autoload、前端控制器的引导),也不包含别的进程/扩展做的事(php-fpm 的请求解析、nginx 侧处理)。正常结束、exit、未捕获的 Error / 异常都会到达止点;SIGKILL / OOM killer 不会——采样状态随进程消失,不会残留给下一个请求。收窄范围靠配置项 ignore_url_arr(对 uri() 子串匹配,不改代码生效)。
4. 用 php -S 真跑一次:
# public/index.php 顶部有 XhprofBootstrap::start(),并把它当前端控制器
php -S 127.0.0.1:8000 -t public public/index.php
访问 http://127.0.0.1:8000/ 产生数据,再访问 http://127.0.0.1:8000/xhprof 看报告页——两者在同一个进程里,资源也一并验证到。
Yii2
适用于 Yii2(yiisoft/yii2 ^2.0,PHP >= 8.0)。与 Yii3 不是同一个框架:Yii3 是 PSR-15 重写版,Yii2 有自己的一套 yii\web\Request / Response 与应用生命周期,入口类也因此不同。
1. 注册引导类 — config/web.php:
'bootstrap' => [ [ 'class' => \ErikWang2013\Xhprof\Yii2\XhprofBootstrap::class, 'config' => ['auth_token' => 'xxx'], // 可选,键集见 src/Yii2/config/xhprof.php ], ],
这是 Yii2 官方扩展点 yii\base\BootstrapInterface 的标准注册形状:数组定义里的 config 键会由容器按公有属性赋给入口类(cache / logger 也是同样的注入点,传实例即可替换默认的 Redis 适配器与 error_log)。
2. 报告页与静态资源 — 无需注册控制器、无需注册路由、无需改 urlManager:引导类在 Application::EVENT_BEFORE_REQUEST 上判断请求路径,命中报告路径 /xhprof 直接输出报告页并结束请求,命中资源路径(前缀从配置项 assets_url 读,默认 /xhprof-assets)直接输出静态资源。两条路径都在采样开始前短路。
3. 采样窗口 = EVENT_BEFORE_REQUEST → EVENT_AFTER_REQUEST。注意 Yii2 的 EVENT_AFTER_REQUEST 在响应发出之前触发(base/Application.php 的 run()),所以「发送响应」本身不在窗口内。异常路径靠 shutdown 兜底:run() 只捕获 ExitException,业务抛出的其它异常会让 EVENT_AFTER_REQUEST 永不触发,因此起表时同时注册一次 register_shutdown_function(与 Symfony / Joomla 两家同形)。
4. 控制台应用零影响 — yii\console\Application 不覆写 run(),所以 CLI 命令(cron、迁移、队列)也会触发 EVENT_BEFORE_REQUEST;引导类在 bootstrap() 里先判 instanceof yii\web\Application,控制台一条钩子都不挂。
5. 客户端 IP 用框架语义 — getRealIp() 走 Request::getUserIP():Yii2 默认把 X-Forwarded-For 这类转发头按 secureHeaders 滤掉(安全默认),所以反代部署下拿到的是 REMOTE_ADDR;要让报告页记录真实客户端 IP,得在应用的 request 组件上配 trustedHosts(配了之后 Yii2 取的是「从右往左第一个不可信地址」,与其余适配器「无条件取首段」刻意不同)。这是框架自己的安全判定,本包不替站点决定信任谁。
6. 配置 — 默认值在包内 src/Yii2/config/xhprof.php,用第 1 步里的 config 键覆盖;redis 子数组可选(不注入 cache 时用它直连 phpredis,键名与其余框架一致:host / port / password / database / timeout)。
配置项说明
所有框架共用以下配置项:
| 配置 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enable |
bool | true |
是否启用性能分析 |
sample_rate |
float | 1.0 |
按比例采样:每个请求以该概率记录(如 0.05 = 5% 请求被采样);1.0 = 全采,<=0 或 false = 不采 |
trigger_token |
string|null | null |
按需触发采样:配置后,带请求头 X-Xhprof-Token: <该值> 的请求强制采样(无视 sample_rate,0 也采);null 或空串 = 关闭,该请求头完全被无视。只认请求头、不认 query(query 会写进访问日志与 Referer)。它能强制任意请求全采样,密钥必须是够长的随机串且只发给可信的人 |
auth_basic |
string|null | null |
HTTP Basic 凭据(user:password,第一个冒号分隔、密码可含冒号)。与 auth_token 是或关系:任一配置即生效、任一通过即放行;都不配 = 不鉴权。Apache+CGI/FastCGI 默认剥离 Authorization 头(需 CGIPassAuth On,2.4.13+),nginx+php-fpm 不受此限 |
ip_allowlist |
array | [] |
报告页 IP 白名单,逐字比对:不支持 CIDR 网段、不做 IPv6 规范化(2001:0db8::1 与 2001:db8::1 是两个字符串)。空 = 关闭;写得不是数组 = 一律拒绝(fail closed,记一条 error 日志)。取值来自 getRealIp(),需与 trusted_proxies 一起理解 |
trusted_proxies |
array | [] |
部署声明,不是技术强制:声明「我前面有可信代理」后,ip_allowlist 才接受来自 X-Forwarded-For/X-Real-IP 的客户端 IP。多数适配器无条件取转发头——声明了也挡不住伪造 XFF,仅当部署在可信代理之后才安全 |
webhook_url |
string|null | null |
慢请求(wt >= view_wtred)落库后 POST JSON(run_id/uri/wt/ct/ip/time)到该地址。留空 = 不发送。不是队列:不等响应、无重试、无落盘补偿,端点慢或挂掉只丢这一条通知 |
sample_cli |
bool | false |
CLI/无 HTTP 请求也采样:true 时落库的 request_uri 记为 cli:<脚本名>;false = 一律忽略(默认,含队列 worker 与定时任务)。能否生效取决于入口:Laravel 已内置(队列监听器 + XhprofCli,见 Laravel 一节),原生 PHP 入口天然按 CLI 形态工作(无 HTTP 依赖);其余框架的入口是 HTTP 专用,需自行包一层 |
symbol_lookup_url |
string|null | null |
源码链接模板:报告页渲染 <模板>?symbol=<urlencoded 函数名>;null/空 = 不显示链接 |
max_runs_per_minute |
int|null | null |
自适应预算:每分钟最多记录多少条(分钟桶计数,超出不采);null/非正数 = 关闭。缓存不可用/抛异常时 fail-open(照常按 sample_rate);触发采样不受它限制 |
time_limit |
int | 0 |
仅记录响应超过 n 秒的请求,0 表示全部 |
log_num |
int | 1000 |
最大记录条数 |
view_wtred |
int | 3 |
列表耗时超过 n 秒标红 |
ignore_url_arr |
array | ["/xhprof"] |
忽略的 URL 路径 |
assets_url |
string | /xhprof-assets |
静态资源 URL 前缀 |
auth_token |
string|null | null |
设置后报告页必须带 ?token=xxx 才能访问。默认 null 即不鉴权:报告页与静态资源由入口类在宿主鉴权之前接管(代价见「报告页与静态资源」一节),未设置时任何能访问到该路径的人都能读到全部 run 的请求 URI、来源 IP 与函数名——公网/多租户部署必须设置;未设置时每次渲染记一条警告日志 |
key_prefix |
string | xhprof |
Redis key 前缀,多项目共用 Redis 时务必改成各自独立的值 |
log_ttl |
int | 604800 |
性能数据保留时间(秒),默认 7 天 |
locale |
string|null | null |
报告页语言:zh_CN/en/ko/ru/de/fr/es/pt/ar/hi/bn/id/ja;null = 跟随浏览器 Accept-Language,都匹配不上则中文;任意语言下都可用 ?lang=xx 临时覆盖 |
各配置项在不同框架上的已知限制见验证与已知限制。
调低 sample_rate 是唯一的按比例降压手段(0.05 = 只记录 5% 的请求);ignore_url_arr 仍是整条路径的兜底,两者可叠加。判定发生在采样入口(每次请求一次),不影响已存数据的读取与保留。非法值(如 '5%'、'disabled')按 1.0 处理:宁可多采,也不静默变成「什么都不采」,让报告页看起来像坏了。
要清理性能数据:只清空列表页用 DEL <prefix>:run_id——数据键会随 log_ttl 自然过期,索引里的悬空 id 会被列表跳过;全清则把 <prefix>:request_log:* 与 <prefix>:xhprof_log:* 扫出来连同索引一起删(DEL 不接受通配符,先用 redis-cli --scan --pattern '<prefix>:*' 列出再删,别用 KEYS)。索引列表没有 TTL 是刻意的:它有界于 log_num,且只是指向数据键的指针(<prefix> 即本项目配置的 key_prefix 值)。
触发采样(trigger_token)
按需触发与按比例采样是两条独立轴,先判触发、再抽签:配上 trigger_token 后,生产环境可以把 sample_rate 压到 0(平时完全不采),要排查时给某个请求带上 X-Xhprof-Token 头,这次请求就会被完整采样。密钥比较用常量时间的 hash_equals;只在请求头上认,不要用 query 传(会写进访问日志、Referer 与浏览器历史)。触发不绕过 ignore_url_arr(报告页/静态资源请求即使带密钥也照常跳过),enable: false 依然是总开关。
报告页鉴权(auth_token 与 auth_basic)
auth_token(?token=xxx)与 auth_basic(HTTP Basic)是或关系:任一配置即生效、任一通过即放行;都不配 = 不鉴权(默认,每次渲染记一条警告日志)。Basic 凭据形如 user:password(第一个冒号分隔、密码可含冒号,用户名与密码两段都用 hash_equals 比对);配了 Basic 而校验不通过时返回 401 并带 WWW-Authenticate——这是浏览器弹出凭据框的唯一触发方式,只用 token 且不通过则返回 403。默认不鉴权是刻意的:报告页由入口类在宿主鉴权之前接管,未配置时任何能访问到该路径的人都能读到全部 run 的请求 URI、来源 IP 与函数名,公网/多租户部署必须配上其中之一。部署坑:Apache + CGI/FastCGI 默认剥离 Authorization 头,Basic 会永远校不过(表现是一直 401)——需要 CGIPassAuth On(2.4.13+)或等效的转发变量;nginx + php-fpm 不受此限。
IP 白名单与可信代理(ip_allowlist / trusted_proxies)
白名单是逐字比对:不支持 CIDR 网段,也不做 IPv6 规范化(2001:0db8::1 与 2001:db8::1 是两个不同的字符串);空 = 关闭;写得不是数组 = 一律拒绝并记一条 error 日志(fail closed——静默关闭等于悄悄丢掉一层安全控制)。判定来源是适配器的 getRealIp(),而多数适配器收到 X-Forwarded-For / X-Real-IP 时无条件取转发头:直接拿它比对,任何客户端都能自报地址绕过白名单。所以还看 trusted_proxies:IP 值恰好来自转发头时,要求 trusted_proxies 非空,否则拒绝并记日志。这是部署声明,不是技术强制:声明了也挡不住伪造的 XFF,仅当部署在自己控制的代理之后才安全,中间跳数是否可信由你的代理配置负责。白名单闸门跑在凭据校验之前(拒绝即 403)。
慢请求 webhook(webhook_url)
响应耗时 wt >= view_wtred 的 run 落库后,向该地址 POST 一份 JSON(字段:run_id / uri / wt / ct / ip / time)。留空 = 不发送。它不是队列:fire-and-forget——连上、写完请求即断,不等响应、不读状态码,无重试、无落盘补偿,端点慢或挂掉只丢这一条通知(连接超时压到 200ms,DNS 解析不受此限);任何失败只记一条 error 日志,绝不影响业务请求。列表页标红用的是严格 >,webhook 条件是 >=,边界差一档。
自适应预算(max_runs_per_minute)
每分钟最多记录多少条:计数的是走到采样入口的请求数(含没抽中的,判在抽签之前),超出即不采、下一分钟自动清零;null/非正数 = 关闭。计数走缓存:Redis 里会出现 <key_prefix>:budget:<YmdHi> 键(如 xhprof:budget:202610032316),首次 incr 时设 120 秒 TTL、过期自然清零——运维排查时看到它属于正常现象。缓存不可用/抛异常时 fail-open:照常按 sample_rate 采样,绝不因预算机制让请求失败或让采样静默停摆。触发采样不受它限制:拿着密钥来排查的人不该被预算挡在门外(判定顺序:触发 → 预算 → 抽签)。
报告页的语言切换器
导航右侧的下拉列出 13 种语言的自称(取自各词表里的 _meta.name,如「한국어」「日本語」)。每个选项的链接由当前页面的查询串生成(XhprofLib::report_url()),所以 ?token=、排序、run 等参数都会跟着走;切换语言不会离开当前视图——在 run 报告页换语言仍停在同一个 run 上。
报告页的诊断区
报告页正文最上面那块卡片就是「诊断结论」(在操作栏与 run 说明之下):先列「为什么慢」(最多 3 条归因),再列「其他发现」(最多 3 条体检项)。每条结论后的「查看」链接跳到该方法详情页;递归(R4)只在裸名确实在符号表里时才给链接——xhprof 把递归展开成 fib@1/fib@2,若符号表里只剩展开后的名字,按 fib 去查就会落空。六条规则与阈值:
- R1 自身耗时 ≥ 请求总耗时的 10%;
- R2 调用次数 ≥ 1000;
- R3 单条边的调用次数 ≥ 500,且被调方自身耗时 ≥ 请求总耗时的 5%;
- R4 同名符号出现在 ≥ 2 个不同深度(递归);
- R5 自身内存峰值 ≥ 全局内存峰值的 30%;
- R6 自身耗时 > 总耗时(
excl_wt > wt,逻辑上不可能)——数据完整性探针,健康数据下不触发。
阈值写死在 src/Core/Analysis/Analyzer.php 的常量里,目前没有配置项能调整或关闭诊断区(enable 关掉后没有采样数据,自然也没有诊断)。它只在顶层单 run 视图出现:diff 对比视图与函数详情页都不渲染——那两处传入的 $symbol_tab/$totals 不是单 run 的值(diff 模式下是 run2 − run1 的增量),据此分析没有意义。
手动初始化
如果自动检测框架失败,可以手动注入适配器:
use ErikWang2013\Xhprof\Core\Xhprof; Xhprof::bootstrap( new MyRequestAdapter($request), new MyResponseAdapter($response), new MyConfigAdapter(), new MyCacheAdapter(), new MyLoggerAdapter() );
除 autoDetect() 认识的那四个框架之外,其余八个(Yii2 / Yii3 / Symfony / Slim 4 / WordPress / Joomla / Drupal / 原生 PHP)都不要调用无参 Xhprof::bootstrap()——无参调用走 autoDetect(),它只认识 webman / Laravel / ThinkPHP / Hyperf 四个分支,在这八个上会直接抛 Unsupported framework。必须像上面的例子一样显式传入 5 个适配器(各框架配套的入口类已经替你做完了这件事)。
架构与设计思路
Core 只通过 5 个契约访问框架,5 个契约都在 src/Core/Contract/:
| 契约 | 方法 | 用途 |
|---|---|---|
RequestInterface |
get() all() method() header() host() uri() url() getRealIp() |
读请求参数、判报告路径、拼报告页链接 |
ResponseInterface |
withBody() withHeaders() withStatus() file() send() |
输出报告页、静态资源与 400/403 |
ConfigInterface |
get() |
读插件配置:get('xhprof') 取整块,get('xhprof.assets_url') 取叶子 |
CacheInterface |
get() set() mget() incr() lPush() rPop() lRange() del() decr() |
Redis 读写 |
LoggerInterface |
error() |
扩展缺失、落库失败的告警 |
每个框架提供 5 个适配器实现这 5 个接口,由 Xhprof::bootstrap() 登记进 Core。框架相关的知识全部收在该框架自己的 src/<Fw>/ 目录里。
Core 对框架仅剩两处耦合:
Xhprof::autoDetect()里那串class_exists()分支(Webman\App→Illuminate\Foundation\Application→think\App→Hyperf\Context\ApplicationContext),只有无参bootstrap()会走到它。- 硬编码的 Hyperf 协程开关:
Xhprof::markHyperfContext()配合\Hyperf\Context\Context的存在性判断,决定适配器存进进程静态属性还是协程 Context。
其余八个框架不经过 autoDetect(),全部走显式注入:入口类自己 new 出 5 个适配器传给 Xhprof::bootstrap($req, $res, $cfg, $cache, $log)。原因是 PSR-7 / 框架自有请求对象的形态只能从请求管线里拿到,无参 bootstrap() 结构上不可能工作;另一个好处是 autoDetect() 保持「四个旧框架」的现状不再膨胀。
上图讲结构:十二个框架各自的入口类、5 个契约、Core 的三层划分,以及仅剩的两处耦合点。
上图讲为什么这么设计:5 条取舍的「决策 / 理由 / 代价」对照,顶栏是「扩展的 8 个框架对 src/Core/ 的改动数 = 0」。
请求生命周期
一次被采样的请求:
- 入口类在采样开始前先判路径:命中报告路径 → 直接输出报告页并返回;命中资源路径 → 直接输出静态资源。这两条路径都不采样,也不进入下面的流程。
XhprofProfiler::isEnabled()读配置里的enable;未开启或扩展缺失则整段跳过。Xhprof::xhprofStart()→XhprofProfiler::start()→xhprof_enable(XHPROF_FLAGS_NO_BUILTINS + XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY)。- 执行业务逻辑。
finally { Xhprof::xhprofStop(); }→xhprof_disable()后由XHProfRunsDefault::save_run()写入 Redis。用finally而不是顺序语句,是为了业务抛异常时采样状态也能被清理并落库。- 浏览器访问报告页,
Xhprof::index()从 Redis 读回数据并渲染。
| 框架 | 采样开始 | 采样结束 |
|---|---|---|
| webman / Laravel / ThinkPHP / Hyperf | 中间件入口(process() / handle()) |
finally |
| Yii3 / Slim 4 | PSR-15 process() |
finally |
| Symfony | kernel.request(优先级 10000) |
kernel.response(优先级 -10000),另有 shutdown 兜底 |
| WordPress | plugins_loaded |
shutdown |
| Joomla | onAfterInitialise |
onAfterRespond,另有 shutdown 兜底 |
| Drupal | http_middleware(priority 1000,最外层) |
finally |
| 原生 PHP(无框架) | 入口文件顶部一行 XhprofBootstrap::start() |
进程 shutdown(register_shutdown_function),另有 stop() 可提前止表 |
| Yii2 | EVENT_BEFORE_REQUEST |
EVENT_AFTER_REQUEST(在响应发出前触发),另有 shutdown 兜底 |
项目结构
xhprof-webman/
├── src/
│ ├── Core/ # 框架无关:契约、报告页、Redis 落库、静态资源
│ │ ├── Contract/ # 5 个契约接口
│ │ ├── XhprofLib/ # 报告页渲染与 run 存储(源自 phacility/xhprof)
│ │ ├── Xhprof.php # 静态门面:bootstrap() / index()
│ │ ├── XhprofProfiler.php # xhprof_enable/disable 与配置
│ │ ├── StaticController.php # /xhprof-assets 静态资源
│ │ ├── MiddlewareTrait.php # Laravel / ThinkPHP 共享的采样包裹逻辑
│ │ └── RedisAdapterTrait.php # 各框架 Redis 适配器的共享实现
│ ├── Webman/ Laravel/ Thinkphp/ Hyperf/ # 既有 4 个框架
│ ├── Yii3/ Symfony/ Slim/ Wordpress/ Joomla/ Drupal/ # 新增 6 个框架
│ ├── Yii2/ # Yii2:BootstrapInterface 入口类与 5 个适配器
│ ├── Native/ # 原生 PHP(无框架):入口类与 5 个适配器
│ └── html/ # 报告页静态资源(css / js / images / pet.svg 站点图标与品牌图标)
├── wordpress/ # mu-plugin 引导文件(带 plugin header)
├── joomla/ # Joomla 插件(CMSPlugin + 清单)
├── drupal/xhprof/ # Drupal 标准模块(info / routing / services + Controller)
├── tools/contracts/ # 独立验证环:对真实框架包校验签名与语义(`legacy-symfony64/`、`legacy-symfony8/` 是两条旧版腿)
├── tools/i18n/ # README 与三张 SVG 的翻译工具链(生成 / 校验 / 自检)
├── docs/i18n/ # 12 份译文产物(英文、韩语、俄语、德语、法语、西班牙语、葡萄牙语、阿拉伯语、印地语、孟加拉语、印尼语、日语)
├── tests/ # PHPUnit:适配器单测、接线测试、Core 单测、14 份 README 的结构一致性
├── demo/ # docker compose 演示(原生 PHP 入口,不装环境看报告页)
└── docs/images/ # README 配图
除 Drupal 外,每个 src/<Fw>/ 目录形状一致:
src/<Fw>/
├── Adapter/{Request,Response,Config,Redis,Log}Adapter.php
├── <入口类>.php
└── config/xhprof.php # 19 个配置键,与其它框架一致
src/Drupal/ 是唯一的例外:它没有 config/,配置改用模块内的 typed config(drupal/xhprof/config/install/xhprof.settings.yml)。
验证与已知限制
能机械证明的
| 项 | 怎么证明 |
|---|---|
| 适配器与入口接线的行为 | tests/Unit/Adapter/*Test.php:enable 落库 / disable 不落库 / 业务抛异常时 finally 仍落库 |
| 十二个框架的配置 key 集一致 | 配置一致性测试(不逐字节比对,注释可不同) |
| 两份 README 逐段镜像 | README 一致性测试:比对 ## / ### 标题序列与代码块数量 |
| 适配器调用的方法真实存在 | tools/contracts/ 验证环(独立 CI job,三条腿:主腿装各框架最新包,独立的 tools/contracts/legacy-symfony64 项目用同一份 Symfony case 跑 6.4,第三条 tools/contracts/legacy-symfony8 跑 Symfony 8.1 + Laravel 13(PHP 8.5)):装真实框架包(Drupal 用真 drupal/core,Joomla 用两个真实 CMS 发布包),对已入环的 10 个框架(Slim / Symfony / Yii3 / Yii2 / Joomla / WordPress / Drupal / Laravel / Webman / ThinkPHP)用反射断言每个方法 / 常量 / 全局函数存在;原生 PHP / Hyperf 未入环,原因各不相同,见下 |
| 适配器语义正确 | 同一验证环用真实类实例化请求与响应后跑适配器,含两条不变量:uri() 不含 scheme/host、file() 之后 withHeaders() 仍生效。环的 SKIP 总数是冻结常量(主腿 2、6.4 腿 0、8 腿 0),两条都在 Joomla:#__extensions.params 的真实读取路径、安装器形态,都需要数据库/安装器才能跑;每个 case 的断言数另有逐腿冻结下限(防编辑型缩水:早退/条件包裹把断言跑少而 status 仍 PASS 时红) |
未自动化验证的(不要当成已验过)
| 项 | 为什么没验 |
|---|---|
| 所有框架的接线(钩子是否真挂上、事件是否真触发) | 单测用的是桩,接线正确性目前只有手工冒烟能确认 |
| Joomla 剩下的两条子项 | 环里仍够不到、且原因都是需要数据库/安装器的那两件事:#__extensions.params 的真实读取路径(PluginHelper::getPlugin() → bootPlugin())、安装器形态(namespacemap 被写过、bootPlugin() 找得到类) |
Symfony 的 kernel.event_subscriber 自动配置 |
需要真实容器编译 |
| 长驻进程下的静态状态串扰 | 已按协程隔离:按请求的渲染态在后端处于协程上下文时存进该环境的 Context(Hyperf / workerman 自动侦测,见 Xhprof::coroutineContextClass());非协程形态用进程级静态(FPM 本就每请求一进程)。验证:Hyperf 用真让出协程测试;workerman 协程在验证环的 Webman 卡里用真服务器 + 真 TCP 两请求交错钉住(A 挂起期间 B 整页渲染,A 醒来仍是自己的 run / 语言 / 指标列) |
| Hyperf 并发协程下的采样串扰 | xhprof 扩展与采样开关都是进程级:同一 worker 上两个协程在 IO 点交错时,先 stop 的取走数据(第二个 stop 幂等 no-op),后完成的 run 被丢、保存的那条混入两个协程的执行记录。渲染态已隔离(上行),采样态无法隔离(扩展语义)。需要干净数据时收紧 sample_rate 或对该场景关闭采样 |
| Laravel Octane(Swoole)下的静态状态串扰 | Octane 的依赖树里没有 workerman/workerman,Workerman\Coroutine 结构上不存在,webman 侧那个后端无法复用;Octane 要的是第三个后端 \Swoole\Coroutine::getContext()(约 10 行 + 一个 class_exists 分支),待有 Swoole 环境再开工 |
| 真实 Redis 读写、浏览器渲染、真实负载下的采样开销 | 真实 Redis 读写已进验证环(cases/Redis.php:真 phpredis + 真 Slim 端到端——业务请求 → 落库 → 列表页 → 报告页);浏览器渲染与真实负载下的采样开销仍超出单测与验证环的范围 |
| 原生 PHP / Hyperf 的适配器签名与语义 | 两家未装入验证环(环覆盖 10 个框架),原因不同:原生 PHP 没有第三方包可装——环的对照物是真实框架包,对它不存在,其适配器语义由 tests/Unit/Adapter/NativeTest.php 用真超全局量 + 真 php -S 往返覆盖(观测面比环的 CLI 更强);Hyperf 能装但跑不起来:真跑 Context::set() 抛 Class "Swoole\Coroutine" not found,环的 CI 只装 xhprof+redis,缺的 ext-swoole 协程运行时是运行前提而非可安装性,故桩仍是包内手写的 tests/Stubs/framework-stubs.php,没有真实包对照 |
手工冒烟清单(每个框架三步)
| 步骤 | 动作 | 期望 |
|---|---|---|
| 1 | 按「框架配置」挂上入口类 | 无报错 |
| 2 | 访问任意业务 URL | Redis 里 xhprof:run_id 的长度 +1 |
| 3 | 访问 /xhprof |
报告页与样式正常显示;/xhprof-assets/js/xhprof_report.js 返回 200 |
原生 PHP 的冒烟:用 php -S 127.0.0.1:8000 -t public public/index.php 起内置服务器(见「原生 PHP」第 4 步),三步照做——报告页与资源跟业务请求在同一个进程里,第 3 步能直接验证到。
已知限制:列表页显示的 request_uri 不含端口
host() 契约的语义是「仅 host,不含端口」(R-2),十二个框架都遵守,只是实现方式不同:PSR-7 的 getHost() 天然不含端口,Joomla / WordPress 手工 parse_url 一次,Webman 与 ThinkPHP 要传严格参数 host(true)(默认参数会把 Host 头连同端口原样返回)。列表页显示的 request_uri 由 host() . uri() 拼成(src/Core/XhprofLib/Utils/XHProfRunsDefault.php),所以非标准端口(如 :8080)部署时,列表里那行 URL 文本不体现端口。链接本身不受影响:列表页与报告页内的链接统一由 XhprofLib::report_url() 生成相对 URL(只含 path + query),点进去是正确的页面,不依赖 host。
assets_url 已支持自定义前缀
静态资源前缀不再是硬编码常量:src/Core/StaticController.php 按配置项 assets_url 匹配资源路径(默认 /xhprof-assets,尾斜杠可有可无)。十二个框架都跟随这个配置:十一家的入口类在采样开始前自行短路并服务资源;Drupal 的默认前缀由模块路由 + Controller 服务、自定义前缀由中间件接管。边界:Laravel、Hyperf、Webman、ThinkPHP 四家不再需要控制器与路由——中间件先跑,按旧版说明注册过的那两条路由只是被遮蔽:既不会报错,也不会再被命中。目录/子路径部署下的剩余限制见下一条 Drupal。
已知限制:Drupal 装在子目录时路径守卫失效
Drupal 装在子目录(如 /sites/app/xhprof)时,路径守卫匹配不上带 base path 的 URI,于是回到「采样但不落库」的行为(默认配置下由 ignore_url_arr 兜住)。
Symfony 6.4 兼容性
Symfony 6.4 的兼容性是实测过的(并因此修掉了两处在 7.4 上看不出的过度拟合:Request 属性在 6.4 无原生类型声明、prepare() 补的 charset 大小写不同)。所有腿都进 CI:主腿 7.x、独立的 tools/contracts/legacy-symfony64 项目(跑同一份 case,不复制)以及第三条 tools/contracts/legacy-symfony8 腿(Symfony 8.1 + Laravel 13,PHP 8.5)——全部都进 tag 门禁。
作者
本包以 MIT 授权发布(见 LICENSE);src/Core/XhprofLib/**、src/html/js/xhprof_report.js、src/html/css/xhprof.css 派生自 phacility/xhprof(Apache-2.0),沿用其条款;第三方前端库清单见 NOTICE。



