PHP Architect logo

Want to check out an issue? Sign up to receive a special offer.

Mago: Blazing Fast Static Analysis for PHP

Posted by on July 31, 2026

Video version: https://youtu.be/ZtAKnN6jWbE

I love static analysis tools to a degree that my co-workers find annoying. Tools like PHPStan and Psalm catch bugs before they ever reach production, and once you get used to that safety net, it is impossible to imagine coding without it. But there is one thing about these tools that drives me a little crazy: they are “slow”.

On a large codebase, a full analysis can take minutes gigabytes of memory. I have split my analysis into chunks and run them in parallel just to get my build times back down to something reasonable. While that solution works, it’s always felt like a hack to me.

Today we are going to look at a potentially better way. It’s called Mago (mah-go), and it is a static analysis tool for PHP written in Rust rather than PHP, which makes it shockingly fast. We will cover how to install it, how to lint your code, how to analyze it for bugs, and how to format it.

What Is Mago?

Mago(mah-go) is a static analysis toolchain for PHP, but unlike other static analysis tools, it is not written in PHP. It is written in Rust. Because it is Rust, it compiles down to a single native binary. You do not need a PHP runtime to run it, you do not need Composer to install it, and there is no bootstrapping overhead every time you kick it off. You just run the binary, and it goes.

Installing Mago

The quickest way to install Mago is with the official installer script (check to see if this has changed too):

curl --proto '=https' --tlsv1.2 -sSf https://carthage.software/mago.sh | bash

That downloads the right binary for your operating system and drops it somewhere on your path. If you prefer a different approach, Mago is also available through the normal Composer install, but there are also Homebrew, Docker, and, I think for the first time I’ve seen it, WinGet, so pick whichever fits your workflow best. On a Mac, brew install mago is about as painless as it gets.

Once it is installed, confirm it worked:

mago --version
mago 1.43.0

There’s also setup-mago GitHub Action that will let it run in your actions.

Setting Up Your Config

The first step is to create a config using:

mago init

This guides you through the process of creating a mago.toml file in the root of your project. If you have used PHPStan or Psalm, think of mago.toml as the equivalent of phpstan.neon or psalm.xml. It is where you tell Mago which directories to scan, what PHP version to target, and how strict you want the rules to be. The .toml format is clean and readable, so even if you have never seen it before, you will figure it out quickly. There are a ton of options but try to keep it simple to start and then add on more options.

Linting Your Code

The mago lint command checks your code against rules covering correctness, consistency, and clarity:

mago lint

Say you have a function like the following:

<?php

function getDiscount(User $user): float
{
    if ($user->isActive()) {
        if ($user->isPremium()) {
            return 0.25;
        }
    }

    return 0.0;
}

You’ll get something like the following:

./mago lint test.php
warning[strict-types]: Missing `declare(strict_types=1);` statement at the beginning of the file.
  ┌─ test.php:1:11<?php
  │ ^^^^^
  │
  = The `strict_types` directive enforces strict type checking, which can prevent subtle bugs.
  = Help: Add `declare(strict_types=1);` at the top of your file.

help[function-name]: Function name `getDiscount` should be in snake case.
   ┌─ test.php:3:10
   │  
 3 │ ╭ function getDiscount($user): float
   │            ^^^^^^^^^^^ Function `getDiscount` is declared here
 4 │ │ {
 5 │ │     if ($user->isActive()) {
 6 │ │         if ($user->isPremium()) {
   · │
11 │ │     return 0.0;
12 │ │ }
   │ ╰─' Function `getDiscount` is defined here
   │  
   = The function name `getDiscount` does not follow snake naming convention.
   = Help: Consider renaming it to `get_discount` to adhere to the naming convention.

warning: found 2 issues: 1 warning(s), 1 help message(s)
 = 1 issues contain auto-fix suggestions

Because this is a safe auto-fix, you can let Mago rewrite it for you:


./mago lint --fix test.php
 WARN Skipped 1 potentially unsafe fixes. Use `--potentially-unsafe` or `--unsafe` to apply them.
 INFO No fixes were applied.
./mago lint --fix  --unsafe test.php
 INFO Successfully applied 1 fixes.

And now our file has declare(strict_types=1) at the top:

<?php

declare(strict_types=1);

More type safety and you didn’t have to touch it by hand.

We’ll have more after this word from our partners.

Analyzing for Bugs

While linting is about style and clarity, and analysis is about catching actual bugs. I like this description that Mago has on their website:

The analyzer builds a semantic model of your entire codebase. It knows what types functions return, what properties classes have, and what can throw. It finds logical impossibilities like calling a method that does not exist on the type at hand.

That is what mago analyze does:

mago analyze

It hunts for problems like type mismatches, dead code, and logic that cannot possibly work.

Here is a quick example. This function promises to return an int but returns a string:

<?php

function totalItems(int $cartCount): int
{
    return $cartCount . " items";
}

Running the analyzer catches it right away:

./mago analyze test.php
error[invalid-return-statement]: Invalid return type for function `totalItems`: expected `int`, but found `truthy-lowercase-string`.
  ┌─ test.php:8:12
  │
8 │     return $cartCount . " items";
  │            ^^^^^^^^^^^^^^^^^^^^^ This has type `truthy-lowercase-string`
  │
  = The type `truthy-lowercase-string` returned here is not compatible with the declared return type `int`.
  = Help: Change the return value to match `int`, or update the function's return type declaration.

error: found 1 issues: 1 error(s)

That is the kind of bug that would, and will, sit quietly until a customer hits it on the busiest day of the year.

Formatting Your Code

Mago also allows us to force a formatting option on our code. The mago fmt command applies deterministic, consistent formatting to your whole codebase:

./mago fmt test.php
 INFO Formatted 1 file(s) successfully.

Run it once, and everyone on your team gets the exact same style with zero arguments about spacing or brace placement.

About That Speed

In order to do what mago is doing with a single command, I have to run 2 other tools, and for some reason it’s finding more issues than I have found in other tools (I’m guessing due to differences in where errors are categorized). In my tests, it takes Mago ~3.4s vs ~13.9s (8.2s + 5.7s) on my ~134k line side project. That’s a large difference, especially given that I run these tools dozens of times a day, which will really add up.

Fitting It Into Your Workflow

I’m a huge fan of running tools like Mago in my CI/CD pipelines and my pre-commit scripts. I’m currently testing it as a replacement for PHPstan and PHP_CodeSniffer in my stack, and I’m excited to see the results coming back faster, but there are some gaps in its implementation I can’t get around that the more mature tools have. Right now I’m using it as a “yes and” stage in my testing so I can get faster feedback but it doesn’t look like a direct replacement just yet.

Gotchas

A few things to keep in mind. First, Mago is still maturing. It is fast and capable, but not every single PHPStan or Psalm rule has an equivalent yet, so you may hit a check you are used to that is not there.

Second, mago fmt reformats your entire codebase in place. Commit your work first so if the formatting does something you did not expect, you can see exactly what changed in your diff and roll it back cleanly.

Finally, remember that Mago reads its settings from mago.toml. It does not look at your existing phpstan.neon or psalm.xml, so you will need to configure it fresh.

What You Need To Know

  1. Mago is a static analysis toolchain for PHP written in Rust
  2. mago lint checks correctness, consistency, and clarity
  3. mago analyze catches type errors and dead code
  4. mago fmt handles formatting.
  5. It’s not as mature as other tools in the ecosystem, but hopefully this will change soon.

Tags: ,
 

Leave a comment

Use the form below to leave a comment:

 

Our Partners

Collaborating with industry leaders to bring you the best PHP resources and expertise