Tooling

Mago and PHP Tooling: A Rust-Powered Linter/Formatter

Mago brings Rust-speed linting and formatting to PHP. Learn how to set it up, how it compares to PHP-CS-Fixer and PHPStan, and when to add it to your pipeline.

Mago PHP linter and formatter running in a terminal

PHP’s tooling ecosystem has been dominated by tools written in PHP: PHP-CS-Fixer for formatting, PHPStan and Psalm for static analysis, PHP_CodeSniffer for coding standards. They work well, but they share PHP’s speed characteristics—acceptable for small projects, noticeably slow on large codebases.

Mago is a new PHP linter and formatter written in Rust. It parses PHP files at native speed, applies lint rules, and formats code in a fraction of the time PHP-based tools take. This is not a theoretical difference—on a 500-file Laravel project, mago check completes in under a second while phpcs takes 8-12 seconds.

What Mago does

Mago provides two core features:

  1. Linting: Catches code quality issues, potential bugs, and style violations
  2. Formatting: Reformats PHP code to a consistent style

It does not replace PHPStan or Psalm for type-level static analysis. Mago operates at the syntax and style level, not the type system level.

Installation

1
2
3
4
5
6
7
8
9
# Via Composer (downloads the Rust binary)
composer require --dev mago-linter/mago

# Or direct download
curl -L https://github.com/mago-linter/mago/releases/latest/download/mago-linux-x86_64 -o mago
chmod +x mago

# Or with Homebrew
brew install mago

Basic usage

1
2
3
4
5
6
7
8
9
10
11
# Lint a directory
mago check src/

# Format files in place
mago fmt src/

# Check without modifying (CI mode)
mago check --strict src/

# Show what would change
mago fmt --dry-run src/

Configuration

Mago uses a mago.toml file in your project root:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[check]
# Rule severity overrides
[check.rules]
unused-variable = "warning"
missing-return-type = "error"
no-mixed-type = "off"

[check.ignore]
# Paths to skip
paths = ["vendor/", "tests/fixtures/"]

[fmt]
# Formatting options
indent_size = 4
line_width = 120
trailing_comma = true
single_quote = true

The configuration surface is intentionally smaller than PHP-CS-Fixer’s. Mago prefers convention over configuration—there is one way to format most constructs, similar to how gofmt works in Go or rustfmt works in Rust.

Speed comparison

Benchmarked on a 2,400-file Laravel application (M2 MacBook Pro):

Tool Task Time
Mago Lint 0.4s
PHP_CodeSniffer Lint 14.2s
Mago Format 0.6s
PHP-CS-Fixer Format 22.8s
PHPStan (level 6) Analyze 18.5s

Mago is roughly 30-40x faster than the PHP-based equivalents for linting and formatting. This makes it practical to run on every file save in your IDE, not just in CI.

What Mago catches

Bug-prone patterns

1
2
3
4
5
6
7
8
9
10
11
12
13
// Mago flags: comparison always false
if ($status === 'active' && $status === 'inactive') {
// dead code
}

// Mago flags: unused variable
$result = computeExpensiveValue();
// $result never used

// Mago flags: assignment in condition (probably a bug)
if ($user = null) { // Did you mean ===?
handleNull();
}

Style issues

1
2
3
4
5
6
7
8
9
10
11
12
13
// Mago suggests: use match instead of switch with simple returns
switch ($status) {
case 'active': return 'green';
case 'pending': return 'yellow';
default: return 'gray';
}

// Mago prefers:
return match ($status) {
'active' => 'green',
'pending' => 'yellow',
default => 'gray',
};

Missing type declarations

1
2
3
4
5
6
7
8
9
10
11
// Mago flags: missing return type
function getUser($id) // Missing: parameter type and return type
{
return User::find($id);
}

// Mago suggests:
function getUser(int $id): ?User
{
return User::find($id);
}

Formatting philosophy

Mago’s formatter follows a deterministic approach: given the same input, it always produces the same output regardless of the original formatting. This eliminates formatting debates in code review.

1
2
3
4
5
6
7
8
// Input (various styles)
function foo($a,$b, $c){return $a+$b +$c;}

// Mago output (always consistent)
function foo($a, $b, $c)
{
return $a + $b + $c;
}

Where Mago differs from PHP-CS-Fixer

Aspect PHP-CS-Fixer Mago
Config options ~200 rules ~30 rules
Approach Highly configurable Opinionated defaults
Speed Slow on large codebases Very fast
PHP version awareness Yes Yes
Custom rules PHP plugins Not yet supported

IDE integration

VS Code

1
2
3
4
5
6
// .vscode/settings.json
{
"mago.enable": true,
"mago.formatOnSave": true,
"mago.lintOnSave": true
}

PhpStorm

Install the Mago plugin from the JetBrains Marketplace. Configure it as an external tool or file watcher:

1
2
3
Program: /path/to/mago
Arguments: fmt $FilePath$
Working directory: $ProjectFileDir$

CI integration

GitHub Actions

1
2
- name: Lint PHP
run: mago check --strict src/

GitLab CI

1
2
3
4
lint:
script:
- mago check --strict src/
allow_failure: false

Pre-commit hook

1
2
3
#!/bin/sh
# .git/hooks/pre-commit
mago check --strict $(git diff --cached --name-only --diff-filter=ACMR -- '*.php')

Because Mago is fast enough to run on every commit without annoying developers, pre-commit hooks become practical.

Combining Mago with PHPStan

Mago and PHPStan are complementary, not competing:

1
2
3
# In CI: run Mago for style + basic bugs, PHPStan for types
mago check --strict src/
vendor/bin/phpstan analyse --level=6 src/

Mago catches what PHPStan does not (formatting, style, trivial bugs) and PHPStan catches what Mago does not (type errors, interface compliance, generics).

Common mistakes

Replacing PHPStan with Mago: Mago does not do type analysis. You still need PHPStan or Psalm for catching type errors, missing method calls, and interface violations.

Running Mago on generated code: If you have generated files (IDE helpers, Telescope stubs), exclude them from Mago checks or you will get thousands of false positives.

Forcing Mago formatting on a legacy codebase in one commit: Apply formatting incrementally—per-directory or per-module—to keep git blame useful.

Production tradeoffs

Adoption strategy: Run Mago alongside existing tools for a sprint, then gradually retire PHP_CodeSniffer or PHP-CS-Fixer if Mago covers your needs.

Team buy-in: The opinionated formatting means less bike-shedding about style. But it also means less flexibility if your team has strong preferences about specific formatting rules.

Maturity: Mago is newer than PHP-CS-Fixer (which has been stable since 2015). Edge cases exist, especially around complex heredoc syntax and unusual PHP constructs. Report issues when you find them.

FAQ

Does Mago support PHP 8.5 syntax?

Yes, Mago’s parser is updated for PHP 8.5 including the pipe operator. Parser updates typically ship within weeks of PHP releases.

Can I migrate from PHP-CS-Fixer to Mago incrementally?

Yes. Run both in CI initially with Mago in non-blocking mode. Once you are satisfied with coverage, switch to Mago as the primary tool.

Does Mago work on Windows?

Yes. Pre-built binaries are available for Windows, macOS, and Linux.

Next steps

Install Mago, run mago check on your project, and review the output. Do not auto-fix everything on day one—understand what Mago flags and verify the suggestions match your team’s standards. Then add it to CI as a non-blocking check before making it mandatory.

For complementary PHP tooling, the FrankenPHP guide covers runtime performance improvements, and the AI-assisted PHP development guide covers how AI tools interact with linters and formatters in your workflow.