Getting started

This guide uses Mago's bundled PHP SDK to create an extension containing one linter rule that reports eval expressions. Extensions may use another language by implementing the same worker protocol.

For a ready-to-customize PHP project, start with the Mago extension template. It includes an extension factory, linter and analyzer examples, corpus tests, PHPUnit tests, Mago configuration, and CI. Clone it, replace the Acme placeholders, and use the walkthrough below to understand each component.

Requirements

  • A Composer project using carthage-software/mago.
  • A PHP version allowed by the installed Mago package. Mago 1.47 supports PHP 8.1 through PHP 8.6.
  • A Mago configuration file in the project root.

The Mago Composer package contains both the executable installer and the version-matched PHP SDK, so extension packages should use it as their SDK dependency.

1. Create the rule

Create src/Mago/Linter/Rules/NoEvalRule.php:

<?php

declare(strict_types=1);

namespace Acme\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: 'acme/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,
        ));
    }
}

targets is important: Mago finds matching nodes in Rust and sends only the required syntax subtrees to PHP. The rule is not called for unrelated nodes.

2. Create the extension factory

Create src/Mago/AcmeExtension.php:

<?php

declare(strict_types=1);

namespace Acme\Mago;

use Acme\Mago\Linter\Rules\NoEvalRule;
use Mago\Sdk\Extension;

final class AcmeExtension
{
    private function __construct() {}

    public static function create(): Extension
    {
        return new Extension(
            identifier: 'acme/project-rules',
            name: 'Acme project rules',
            version: '1.0.0',
            linterRules: [new NoEvalRule()],
        );
    }
}

The extension package should own its identifier, name, version, default rules, analyzer plugins, and reducer wiring. Consumers call create() rather than reconstructing that registration themselves, so they cannot accidentally omit a rule or advertise inconsistent metadata.

Factory parameters should expose intentional, strictly typed package options. Do not make consumers pass the extension identifier or raw rule and plugin lists.

3. Create the worker entrypoint

Create .mago/acme-worker.php:

<?php

declare(strict_types=1);

use Acme\Mago\AcmeExtension;
use Mago\Sdk\Worker;

require dirname(__DIR__) . '/vendor/autoload.php';

(new Worker(
    AcmeExtension::create(),
))->run();

The worker entrypoint must reserve standard output for the SDK protocol. Write diagnostics to standard error instead.

4. Configure the host

Add an extension host to mago.toml:

[extension-hosts.acme]
command = ["php", ".mago/acme-worker.php"]

The table name, acme, is a local host-pool name. It is separate from the globally unique identifier advertised by the extension.

With no workers value, the pool is adaptive: it starts small and may grow up to Mago's configured thread count. The default working directory is the configuration file's directory, so .mago/acme-worker.php resolves from there.

5. Validate registration

Start the host and validate its registration:

mago extension validate

Inspect the registered extension and rules:

mago extension list
mago extension list --json

Run the rule:

mago lint --only acme/no-eval

Because the example rule is enabled by default, a normal mago lint run also executes it.

6. Add analyzer behavior

After creating src/Mago/Analyzer/AcmePlugin.php and src/Mago/Analyzer/Providers/ContainerReturnTypes.php, import both classes in the extension factory and register them there. The consuming worker entrypoint does not change:

public static function create(): Extension
{
    return new Extension(
        identifier: 'acme/project-rules',
        name: 'Acme project rules',
        version: '1.0.0',
        linterRules: [new NoEvalRule()],
        analyzerPlugins: [new AcmePlugin(new ContainerReturnTypes())],
    );
}

The plugin implements Mago\Sdk\Analyzer\Plugin and registers its hooks and providers through PluginRegistry. Continue with Analyzer plugins.

↳ Edit this page →