
Mago 是一个用 Rust 编写的高性能 PHP 工具链,集代码格式化、Linter、静态分析于一体。从 1.47.0 开始,它正式开放了 Extension API,并且官方提供了第一方 PHP SDK。这意味着:PHP 开发者无需学习 Rust,就能给这个 Rust 工具写扩展。
本文将带你从零写一个可用的 Mago 扩展:实现一条自定义 Linter 规则,禁止项目中使用 eval(),并给出帮助提示。所有代码均可在 Mago 1.47+ 环境中直接运行。
Mago 的扩展不是直接加载进 Rust 进程的 DLL/SO,而是运行在独立的 Worker 进程中。Mago 和 Worker 之间通过一套二进制的 worker protocol 通信。
对 PHP 开发者来说,你只需要关心三件事:
概念 | 作用 |
|---|---|
Rule / Plugin | 你的业务逻辑,比如一条 Linter 规则 |
Extension | 一个逻辑包,把多个 Rule / Plugin 打包在一起 |
Worker | 一个长期运行的 PHP 进程,向 Mago 注册 Extension,并接收回调 |
这种设计有几个好处:
• 扩展崩溃不会拖垮 Mago 主进程
• 可以用任何语言实现 Worker,不限定 Rust
• 多 Worker 并行,天然支持多核
如果你还没安装 Mago:
composer require --dev carthage-software/mago |
|---|
安装完成后,确认版本:
./vendor/bin/mago --version# 期望输出 1.47.0 或更高 |
|---|
carthage-software/mago 这个 Composer 包同时包含 Mago 可执行文件安装器和版本匹配的 PHP SDK,所以扩展项目直接依赖它即可。
Mago 1.47 支持 PHP 8.1 到 PHP 8.6。扩展代码也在这个范围内。
mkdir mago-extension-demo && cd mago-extension-democomposer init --no-interaction --name=demo/mago-extension-democomposer require --dev carthage-software/mago |
|---|
注意: 我这里是把扩展代码放到 .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 |
|---|
创建 .mago/extension/Linter/Rules/NoEvalRule.php:
/** * @desc NoEvalRule.php 描述信息 * @author Tinywan(ShaoBo Wan) */declare(strict_types=1); namespace Tinywan\Mago\Linter\Rules; use Mago\Sdk\Linter\LintContext;use Mago\Sdk\Linter\Rule;use Mago\Sdk\Linter\RuleDefinition;use Mago\Sdk\Reporting\Issue;use Mago\Sdk\Reporting\Level;use Mago\Sdk\Syntax\NodeKind; final class NoEvalRule implements Rule{ public function getDefinition(): RuleDefinition { return new RuleDefinition( code: 'tinywan/no-eval', name: 'No eval', description: 'Disallows evaluating dynamically generated PHP code.', defaultLevel: Level::Error, defaultEnabled: true, targets: [NodeKind::EvalConstruct], ); } public function lint(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 里覆盖这个级别。
创建 .mago/extension/TinywanExtension.php:
/** * @desc TinywanExtension.php 描述信息 * @author Tinywan(ShaoBo Wan) */declare(strict_types=1); namespace Tinywan\Mago; use Mago\Sdk\Extension;use Tinywan\Mago\Linter\Rules\NoEvalRule; final class TinywanExtension{ private function __construct() {} public static function create(): Extension { return new Extension( identifier: 'tinywan/project-rules', name: 'Tinywan project rules', version: '1.0.0', linterRules: [new NoEvalRule()], ); }} |
|---|
工厂的作用是把扩展的元数据、规则列表、插件列表封装起来。消费者直接调用 create(),不需要自己拼 Extension 对象。
创建 .mago/tinywan-worker.php:
/** * @desc tinywan-worker.php 描述信息 * @author Tinywan(ShaoBo Wan) */declare(strict_types=1); use Mago\Sdk\Worker;use Tinywan\Mago\TinywanExtension; require dirname(__DIR__) . '/vendor/autoload.php'; (new Worker( TinywanExtension::create(),))->run(); |
|---|
注意:Worker 的标准输出(STDOUT)被 SDK 协议占用,调试信息必须写到标准错误(STDERR)。
在项目根目录创建 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 解析,是一个直接的程序 + 参数数组。
创建 tests/sample.php:
declare(strict_types=1); $code = 'echo 开源技术小栈;';eval($code); // 这里应该触发 tinywan/no-eval |
|---|
./vendor/bin/mago extension validate Validated 1 extension(s) from 1 host(s). |
|---|
如果输出没有错误,说明 Worker 能正常启动并向 Mago 注册。
./vendor/bin/mago extension list |
|---|
你应该能看到类似:
Extension hosts: tinywan (adaptive, up to 4 workers)Registered extensions:Tinywan project rules (tinywan/project-rules) Version: 1.0.0 Linter rules: 1 tinywan/no-eval (error) |
|---|
tinywan/no-eval 后面的 (error) 就是规则的默认级别。
./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 1 issues: 1 error(s) |
|---|
因为规则默认启用,直接执行 mago lint 也会报这个问题。
Mago 的 Issue 支持 withHelp() 和 withNote(),可以给开发者更清晰的指引。
修改 .mago/extension/Linter/Rules/NoEvalRule.php:
public function lint(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 2 issues: 2 error(s) |
|---|
如果你想给出自动修复建议,可以构造 TextEdit:
use Mago\Sdk\Reporting\TextEdit; $context->report( Issue::new( 'Avoid evaluating dynamically generated PHP code.', $context->node->span, ) ->withEdit(TextEdit::replace($context->node->span, '// eval() removed'))); |
|---|
mago.toml 中的扩展主机有很多可配置项:
[extension-hosts.tinywan]enabled = truecommand = ["php", ".mago/tinywan-worker.php"]workers = 0 # 0 表示自适应进程池working-directory = "."inherit-environment = trueenvironment = { APP_ENV = "analysis" }maximum-payload-size = 67108864request-timeout-ms = 30000shutdown-timeout-ms = 250stderr-tail-size = 65536 |
|---|
• workers = 0:自适应进程池,最多到 Mago 线程数。
• 如果你的扩展有外部依赖(比如要连数据库),可以固定 workers = 2。
• environment 可以给 Worker 传环境变量。
扩展里设置的 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 |
|---|
./vendor/bin/mago extension list --json |
|---|
./vendor/bin/mago lint --no-extensions |
|---|
如果关掉扩展就没有问题,说明问题跟扩展无关。
Mago 会保留 Worker 的 STDERR 尾部。你也可以在 Worker 里主动写日志:
fwrite(STDERR, "Tinywan extension initialized.\n"); |
|---|
./vendor/bin/mago lint --only tinywan/no-eval |
|---|
MAGO_LOG=trace ./vendor/bin/mago lint |
|---|
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); use Mago\Sdk\Worker;use Tinywan\Mago\TinywanExtension; require dirname(__DIR__, 2) . '/vendor/autoload.php'; (new Worker( TinywanExtension::create(),))->run(); |
|---|
创建 tests/corpus/src/bad.php:
// @mago-expect lint:tinywan/no-evaleval('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 |
|---|
通过本文,你应该已经掌握了:
• Mago 扩展的基本架构:Worker → Extension → Rule/Plugin
• 把扩展代码放到 .mago/extension/,不污染业务 src/
• 用 PHP SDK 编写一条 Linter 规则
• 理解 defaultLevel 和 extension list 中的 (error) 含义
• 配置 mago.toml 注册扩展主机,并覆盖规则级别
• 验证、运行、调试扩展
• 用 corpus 做回归测试
最重要的是:Mago 的核心是用 Rust 写的,但扩展可以用 PHP 写。这对广大 PHP 开发者来说门槛极低,你可以为团队定制框架特定的规则、分析器插件,甚至整合公司内部的代码规范。