Mago 扩展开发实战指南
Mago 是一个用 Rust 编写的高性能 PHP 工具链,集代码格式化、Linter、静态分析于一体。从 1.47.0 开始,它正式开放了 Extension API,并且官方提供了第一方 PHP SDK。这意味着:PHP 开发者无需学习 Rust,就能给这个 Rust 工具写扩展。
本文将带你从零写一个可用的 Mago 扩展:实现一条自定义 Linter 规则,禁止项目中使用 eval(),并给出帮助提示。所有代码均可在 Mago 1.47+ 环境中直接运行。
01先理解 Mago 扩展的架构
Mago 的扩展不是直接加载进 Rust 进程的 DLL/SO,而是运行在独立的 Worker 进程中。Mago 和 Worker 之间通过一套二进制的 worker protocol 通信。
对 PHP 开发者来说,你只需要关心三件事:
| Rule / Plugin | |
| Extension | |
| Worker |
这种设计有几个好处:
• 扩展崩溃不会拖垮 Mago 主进程
• 可以用任何语言实现 Worker,不限定 Rust
• 多 Worker 并行,天然支持多核
02环境准备
安装 Mago
如果你还没安装 Mago:
composer require --dev carthage-software/mago |
安装完成后,确认版本:
./vendor/bin/mago --version # 期望输出 1.47.0 或更高 |
carthage-software/mago 这个 Composer 包同时包含 Mago 可执行文件安装器和版本匹配的 PHP SDK,所以扩展项目直接依赖它即可。
PHP 版本要求
Mago 1.47 支持 PHP 8.1 到 PHP 8.6。扩展代码也在这个范围内。
初始化测试项目
mkdir mago-extension-demo && cd mago-extension-demo composer init --no-interaction --name=demo/mago-extension-demo composer require --dev carthage-software/mago |
03项目结构
注意: 我这里是把扩展代码放到 .mago/extension/
为了让扩展不污染业务源码,我们把扩展相关代码统一放到 .mago/extension/。业务 src/ 保持干净:
mago-extension-demo/ ├── .mago/ │ ├── tinywan-worker.php # Worker 入口 │ └── extension/ # 扩展源码目录 │ ├── TinywanExtension.php │ └── Linter/ │ └── Rules/ │ └── NoEvalRule.php ├── src/ # 业务代码,保持干净 ├── tests/ │ └── sample.php # 用来测试的坏代码 ├── mago.toml # Mago 配置 └── composer.json |
然后在 composer.json 里,用 autoload-dev 把 .mago/extension/ 注册到命名空间:
{ "name": "demo/mago-extension-demo", "autoload": { "psr-4": { "Demo\\App\\": "src/" } }, "autoload-dev": { "psr-4": { "Tinywan\\Mago\\": ".mago/extension/" } }, "require-dev": { "carthage-software/mago": "^1.47" } } |
执行:
composer dump-autoload |
04编写扩展代码
写 Linter Rule
创建 .mago/extension/Linter/Rules/NoEvalRule.php:
/** * @desc NoEvalRule.php 描述信息 * @author Tinywan(ShaoBo Wan) */ declare(strict_types=1); namespaceTinywan\Mago\Linter\Rules; useMago\Sdk\Linter\LintContext; useMago\Sdk\Linter\Rule; useMago\Sdk\Linter\RuleDefinition; useMago\Sdk\Reporting\Issue; useMago\Sdk\Reporting\Level; useMago\Sdk\Syntax\NodeKind; finalclassNoEvalRuleimplementsRule { publicfunctiongetDefinition(): RuleDefinition { returnnewRuleDefinition( code: 'tinywan/no-eval', name: 'No eval', description: 'Disallows evaluating dynamically generated PHP code.', defaultLevel: Level::Error, defaultEnabled: true, targets: [NodeKind::EvalConstruct], ); } publicfunctionlint(LintContext $context): void { // 合作式取消检查,长循环中建议加上 $context->cancellation->throwIfCancelled(); $context->report(Issue::new( 'Avoid evaluating dynamically generated PHP code.', $context->node->span, )); } } |
关键点:
•code 必须全局唯一,建议用 vendor/rule-name 格式。
•targets 决定了 Mago 会把哪些语法节点发给你的规则。这里我们只关心 EvalConstruct,也就是 eval 表达式。
•LintContext 里包含当前节点、源文件、父节点、取消令牌等。
•defaultLevel 是规则触发 issue 的默认级别。
Mago 的 Level 有四种:
| extension list | ||
|---|---|---|
| Level::Help | (help) | |
| Level::Note | (note) | |
| Level::Warning | (warning) | |
| Level::Error | (error) |
所以当你看到:
tinywan/no-eval (error) |
它表示这条规则默认会报 Error 级别的诊断。项目可以在 mago.toml 里覆盖这个级别。
写 Extension 工厂
创建 .mago/extension/TinywanExtension.php:
/** * @desc TinywanExtension.php 描述信息 * @author Tinywan(ShaoBo Wan) */ declare(strict_types=1); namespaceTinywan\Mago; useMago\Sdk\Extension; useTinywan\Mago\Linter\Rules\NoEvalRule; finalclassTinywanExtension { privatefunction__construct() {} publicstaticfunctioncreate(): Extension { returnnewExtension( identifier: 'tinywan/project-rules', name: 'Tinywan project rules', version: '1.0.0', linterRules: [newNoEvalRule()], ); } } |
工厂的作用是把扩展的元数据、规则列表、插件列表封装起来。消费者直接调用 create(),不需要自己拼 Extension 对象。
写 Worker 入口
创建 .mago/tinywan-worker.php:
/** * @desc tinywan-worker.php 描述信息 * @author Tinywan(ShaoBo Wan) */ declare(strict_types=1); useMago\Sdk\Worker; useTinywan\Mago\TinywanExtension; requiredirname(__DIR__) . '/vendor/autoload.php'; (newWorker( TinywanExtension::create(), ))->run(); |
注意:Worker 的标准输出(STDOUT)被 SDK 协议占用,调试信息必须写到标准错误(STDERR)。
05配置 Mago
在项目根目录创建 mago.toml:
[source] paths = ["src", "tests"] [extension-hosts.tinywan] command = ["php", ".mago/tinywan-worker.php"] |
这里 [extension-hosts.tinywan] 的 tinywan 是本地 host pool 名称,跟 Extension 的全局 identifier 是两回事。
command 不会被 shell 解析,是一个直接的程序 + 参数数组。
06准备测试代码
创建 tests/sample.php:
declare(strict_types=1); $code = 'echo 开源技术小栈;'; eval($code); // 这里应该触发 tinywan/no-eval |
07运行扩展
验证扩展注册
./vendor/bin/mago extension validate Validated1extension(s) from1host(s). |
如果输出没有错误,说明 Worker 能正常启动并向 Mago 注册。
查看已注册的规则
./vendor/bin/mago extension list |
你应该能看到类似:
Extensionhosts: tinywan (adaptive, up to 4 workers) Registeredextensions: Tinywan project rules (tinywan/project-rules) Version: 1.0.0 Linterrules: 1 tinywan/no-eval (error) |
tinywan/no-eval 后面的 (error) 就是规则的默认级别。
运行 Linter
./vendor/bin/mago lint --only tinywan/no-eval |
预期输出:
error[tinywan/no-eval]: Avoid evaluating dynamically generated PHP code. ┌─ tests/sample.php:10:1 │ 10 │ eval($code); // 这里应该触发 tinywan/no-eval │ ^^^^^^^^^^^ error: found 1issues: 1error(s) |
因为规则默认启用,直接执行 mago lint 也会报这个问题。
08进阶:给规则加上帮助提示
Mago 的 Issue 支持 withHelp() 和 withNote(),可以给开发者更清晰的指引。
修改 .mago/extension/Linter/Rules/NoEvalRule.php:
publicfunctionlint(LintContext $context): void { $context->cancellation->throwIfCancelled(); $context->report( Issue::new( 'Avoid evaluating dynamically generated PHP code.', $context->node->span, ) ->withHelp('Consider refactoring to avoid runtime code evaluation.') ->withNote('eval() is dangerous and often disabled in secure environments.') ); } |
重新运行 mago lint,就会看到 help 和 note 信息。
error[tinywan/no-eval]: Avoid evaluating dynamically generated PHP code. ┌─ tests/sample.php:10:1 │ 10 │ eval($code); // 这里应该触发 tinywan/no-eval │ ^^^^^^^^^^^ │ = eval() is dangerous and often disabled in secure environments. = Help: Consider refactoring to avoid runtime code evaluation. error: found 2issues: 2error(s) |
如果你想给出自动修复建议,可以构造 TextEdit:
useMago\Sdk\Reporting\TextEdit; $context->report( Issue::new( 'Avoid evaluating dynamically generated PHP code.', $context->node->span, ) ->withEdit(TextEdit::replace($context->node->span, '// eval() removed')) ); |
09配置扩展主机的更多选项
mago.toml 中的扩展主机有很多可配置项:
[extension-hosts.tinywan] enabled = true command = ["php", ".mago/tinywan-worker.php"] workers = 0# 0 表示自适应进程池 working-directory = "." inherit-environment = true environment = { APP_ENV = "analysis" } maximum-payload-size = 67108864 request-timeout-ms = 30000 shutdown-timeout-ms = 250 stderr-tail-size = 65536 |
•workers = 0:自适应进程池,最多到 Mago 线程数。
• 如果你的扩展有外部依赖(比如要连数据库),可以固定 workers = 2。
•environment 可以给 Worker 传环境变量。
10覆盖规则的默认级别
扩展里设置的 defaultLevel 只是默认值。使用项目的 mago.toml 可以覆盖它。
比如把 tinywan/no-eval 从 Error 降级为 Warning:
[linter] default = true [linter.rules] "tinywan/no-eval" = { level = "warning" } |
再次运行:
./vendor/bin/mago extension list |
显示还是 (error),因为 extension list 展示的是扩展自己声明的默认值。但实际 mago lint 输出会变成 warning[tinywan/no-eval]。
如果想彻底禁用这条规则:
[linter.rules] "tinywan/no-eval" = false |
11调试扩展
查看扩展是否注册成功
./vendor/bin/mago extension list --json |
关闭扩展对比
./vendor/bin/mago lint --no-extensions |
如果关掉扩展就没有问题,说明问题跟扩展无关。
查看 Worker 错误输出
Mago 会保留 Worker 的 STDERR 尾部。你也可以在 Worker 里主动写日志:
fwrite(STDERR, "Tinywan extension initialized.\n"); |
只跑指定规则
./vendor/bin/mago lint --only tinywan/no-eval |
使用 trace 日志
MAGO_LOG=trace ./vendor/bin/mago lint |
12测试扩展:用 Corpus 做回归测试
Mago 官方推荐用真实 CLI 跑小型 corpus 来做回归测试。
创建 tests/corpus/mago.toml:
[source] paths = ["src"] [extension-hosts.tinywan] command = ["php", "../worker.php"] |
创建 tests/corpus/worker.php:
declare(strict_types=1); useMago\Sdk\Worker; useTinywan\Mago\TinywanExtension; requiredirname(__DIR__, 2) . '/vendor/autoload.php'; (newWorker( TinywanExtension::create(), ))->run(); |
创建 tests/corpus/src/bad.php:
// @mago-expect lint:tinywan/no-eval eval('echo 1;'); |
运行:
./vendor/bin/mago --workspace tests/corpus lint --reporting-format count |
如果 @mago-expect 被满足,输出为 0。如果规则没触发,会报 unfulfilled-expect。
也可以用 baseline:
./vendor/bin/mago --workspace tests/corpus lint --generate-baseline ./vendor/bin/mago --workspace tests/corpus lint --verify-baseline |
13总结
通过本文,你应该已经掌握了:
• Mago 扩展的基本架构:Worker → Extension → Rule/Plugin
• 把扩展代码放到 .mago/extension/,不污染业务 src/
• 用 PHP SDK 编写一条 Linter 规则
• 理解 defaultLevel 和 extension list 中的 (error) 含义
• 配置 mago.toml 注册扩展主机,并覆盖规则级别
• 验证、运行、调试扩展
• 用 corpus 做回归测试
最重要的是:Mago 的核心是用 Rust 写的,但扩展可以用 PHP 写。这对广大 PHP 开发者来说门槛极低,你可以为团队定制框架特定的规则、分析器插件,甚至整合公司内部的代码规范。
夜雨聆风