Write PHP Extensions for the Rust‑Based Mago Toolchain – A Hands‑On Guide
This guide walks PHP developers through creating a custom Mago extension that adds a Linter rule to forbid eval(), covering Mago’s worker‑process architecture, project setup, extension and rule implementation, configuration, testing, debugging, and advanced options such as help messages, auto‑fixes, and regression testing with a corpus.
Mago is a high‑performance PHP toolchain written in Rust that bundles code formatting, linting, and static analysis. Since version 1.47.0 it exposes an Extension API and provides an official PHP SDK, allowing PHP developers to write extensions without learning Rust.
1. Understand Mago’s Extension Architecture
The extension runs in an isolated Worker process that communicates with the Mago core via a binary worker protocol. Three core concepts are:
Rule / Plugin : the business logic, e.g., a Linter rule.
Extension : a package that bundles multiple rules or plugins.
Worker : a long‑running PHP process that registers the extension and receives callbacks.
Benefits of this design include crash isolation, language‑agnostic workers, and native multi‑core parallelism.
2. Prepare the Environment
Install Mago via Composer: composer require --dev carthage-software/mago Verify the version (≥ 1.47.0):
./vendor/bin/mago --version # expected output: 1.47.0 or higherThe package contains the Mago executable and a matching PHP SDK.
3. Project Structure
Create a demo project and register the extension code under .mago/extension/ so that the business source under src/ stays clean:
mago-extension-demo/
├── .mago/
│ ├── tinywan-worker.php # Worker entry point
│ └── extension/
│ ├── TinywanExtension.php
│ └── Linter/Rules/NoEvalRule.php
├── src/ # Business code (unchanged)
├── tests/
│ └── sample.php # Sample code that triggers the rule
├── mago.toml # Mago configuration
└── composer.jsonAdd the extension namespace to composer.json:
{
"autoload-dev": {
"psr-4": {
"Tinywan\\Mago\\": ".mago/extension/"
}
},
"require-dev": {
"carthage-software/mago": "^1.47"
}
}Run composer dump-autoload to register the autoloader.
4. Implement the Linter Rule
Create .mago/extension/Linter/Rules/NoEvalRule.php:
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)
);
}
}Key points: code must be globally unique, e.g., vendor/rule-name. targets selects the syntax nodes the rule receives; here it is EvalConstruct (the eval expression). LintContext provides the current node, source file, parent node, and a cancellation token. defaultLevel defines the severity (Help, Note, Warning, Error).
5. Create the Extension Factory
File .mago/extension/TinywanExtension.php:
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()]
);
}
}The factory bundles metadata and the rule list; callers simply invoke TinywanExtension::create().
6. Write the Worker Entrypoint
File .mago/tinywan-worker.php:
declare(strict_types=1);
use Mago\Sdk\Worker;
use Tinywan\Mago\TinywanExtension;
require dirname(__DIR__) . '/vendor/autoload.php';
(new Worker(TinywanExtension::create()))->run();Note: the worker’s STDOUT is reserved for the SDK protocol; debugging output must go to STDERR.
7. Configure Mago
In the project root, create mago.toml:
[source]
paths = ["src", "tests"]
[extension-hosts.tinywan]
command = ["php", ".mago/tinywan-worker.php"]
workers = 0 # adaptive pool, up to Mago thread count
working-directory = "."
inherit-environment = true
environment = { APP_ENV = "analysis" }
maximum-payload-size = 67108864
request-timeout-ms = 30000
shutdown-timeout-ms = 250
stderr-tail-size = 65536The workers value of 0 lets Mago scale the pool automatically; set a fixed number if the extension needs external resources.
8. Test the Extension
Create tests/sample.php that triggers the rule:
declare(strict_types=1);
$code = 'echo 开源技术小栈;';
eval($code); // this should trigger tinywan/no-evalValidate registration: ./vendor/bin/mago extension validate List registered extensions: ./vendor/bin/mago extension list Run the linter for the specific rule: ./vendor/bin/mago lint --only tinywan/no-eval Expected output shows an error[tinywan/no-eval] with the message “Avoid evaluating dynamically generated PHP code.”
9. Add Help and Note Messages
Modify lint() to chain withHelp() and withNote():
$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.')
);Rerunning the linter now displays the help and note sections.
10. Provide an Auto‑Fix
Use withEdit() and TextEdit::replace() to suggest a replacement:
$context->report(
Issue::new('Avoid evaluating dynamically generated PHP code.', $context->node->span)
->withEdit(TextEdit::replace($context->node->span, '// eval() removed'))
);11. Override Rule Severity
In mago.toml you can change the default level:
[linter.rules]
"tinywan/no-eval" = { level = "warning" }Running mago lint will now emit warning[tinywan/no-eval] while extension list still shows the original (error) declaration.
12. Debugging Tips
Disable extensions with ./vendor/bin/mago lint --no-extensions to isolate issues.
View the worker’s STDERR tail via ./vendor/bin/mago extension list --json or write directly with fwrite(STDERR, "msg\n").
Enable trace logs: MAGO_LOG=trace ./vendor/bin/mago lint.
13. Regression Testing with a Corpus
Create a small corpus under tests/corpus/:
# tests/corpus/mago.toml
[source]
paths = ["src"]
[extension-hosts.tinywan]
command = ["php", "../worker.php"]Write a test file tests/corpus/src/bad.php containing an @mago-expect lint:tinywan/no-eval comment. Run:
./vendor/bin/mago --workspace tests/corpus lint --reporting-format countIf the expectation is met, the exit code is 0; otherwise an unfulfilled-expect error is reported. Baselines can be generated and verified with --generate-baseline and --verify-baseline.
14. Summary
The article demonstrates how PHP developers can extend the Rust‑based Mago toolchain without touching Rust code, covering architecture, project layout, rule implementation, configuration, testing, debugging, and advanced features such as help messages, auto‑fixes, and corpus‑based regression testing.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
