乐于分享
好东西不私藏

PHP 也能给 Rust 工具写插件了!Mago 扩展开发实战指南

PHP 也能给 Rust 工具写插件了!Mago 扩展开发实战指南

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
你的业务逻辑,比如一条 Linter 规则
Extension
一个逻辑包,把多个 Rule / Plugin 打包在一起
Worker
一个长期运行的 PHP 进程,向 Mago 注册 Extension,并接收回调

这种设计有几个好处:

  扩展崩溃不会拖垮 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)

Version1.0.0

Linterrules1

    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 1issues1error(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.

   = HelpConsider refactoring to avoid runtime code evaluation.

error: found 2issues2error(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 开发者来说门槛极低,你可以为团队定制框架特定的规则、分析器插件,甚至整合公司内部的代码规范。