DEV Community

Cover image for Psalm vs PHPStan in 2026: Which Static Analyzer Fits Which Codebase
Gabriel Anhaia
Gabriel Anhaia

Posted on

Psalm vs PHPStan in 2026: Which Static Analyzer Fits Which Codebase


You're setting up static analysis on a PHP 8.4 codebase for the first time. You open two tabs. One says PHPStan. One says Psalm. Both are free, both parse your code without running it, both have thousands of GitHub stars. Every "which is better" thread ends in a shrug.

The shrug is wrong. The two tools have drifted far enough apart that the right answer depends on what you're building, who's maintaining it, and how much you care about security. This is a decision guide, not a winner.

The one thing that trips everyone up first

The levels run in opposite directions.

PHPStan has levels 0 through 10. Zero is the loosest. Ten is the strictest. You climb up.

Psalm has levels 8 through 1. Eight is the loosest. One is the strictest. You climb down.

<!-- psalm.xml — level 1 is the STRICTEST setting -->
<psalm errorLevel="1"
       xmlns="https://getpsalm.org/schema/config">
    <projectFiles>
        <directory name="src" />
    </projectFiles>
</psalm>
Enter fullscreen mode Exit fullscreen mode
# phpstan.neon — level 10 is the STRICTEST setting
parameters:
    level: 10
    paths:
        - src
Enter fullscreen mode Exit fullscreen mode

Get this backwards and you'll think you turned strictness up when you turned it off. Every team migrating between the two hits it. Write it on a sticky note.

Maturity and maintenance in 2026

This is where the tools split hardest, and it's the part that should weigh most.

PHPStan has a full-time maintainer and a commercial product (PHPStan Pro) funding the work. Releases are frequent. PHP 8.4 property hooks and asymmetric visibility landed in the analyzer close to the language release, and 8.5 support followed the same pattern. If you file a bug, you tend to get a response.

Psalm covers PHP 8.4 and keeps shipping, but its release cadence slowed after its original author moved on from Vimeo. It's community-maintained now. That's not a death sentence. The type engine is excellent and battle-tested. But if you need same-week support for the newest language feature, PHPStan is the safer bet in 2026.

If your codebase leans on brand-new syntax the week it ships, that difference is the whole decision. If you're on a stable 8.3/8.4 baseline, both keep up fine.

Baseline handling

Both tools let you adopt strict analysis on a legacy codebase without fixing everything first. You freeze existing errors into a baseline file, keep CI green, and forbid new errors.

PHPStan writes a NEON baseline:

vendor/bin/phpstan analyse --generate-baseline
Enter fullscreen mode Exit fullscreen mode

Psalm writes an XML baseline, and it gives you a second verb the other tool doesn't:

# create the baseline
vendor/bin/psalm --set-baseline=psalm-baseline.xml

# after fixing some issues, shrink it to match reality
vendor/bin/psalm --update-baseline
Enter fullscreen mode Exit fullscreen mode

That --update-baseline is a real ergonomic win. It rewrites the baseline down to only the errors that still exist, so the file shrinks as you clean up. With PHPStan you regenerate the whole baseline to get the same effect. Small thing, but if you run a long "pay down the debt" campaign, Psalm's flow has less friction.

Both baselines are debt ledgers. They're allowed to shrink and never allowed to grow. Wire that rule into CI whichever tool you pick.

Generics and templates

Both understand generics through docblocks. Both read the neutral @template syntax, and each has a prefixed variant (@phpstan-template, @psalm-template) for tool-specific behavior.

/**
 * @template T
 */
final class TypedCollection
{
    /** @var list<T> */
    private array $items = [];

    /** @param T $item */
    public function add(mixed $item): void
    {
        $this->items[] = $item;
    }

    /** @return list<T> */
    public function all(): array
    {
        return $this->items;
    }
}
Enter fullscreen mode Exit fullscreen mode

Psalm pioneered much of this. It has the deeper bag of advanced types: @psalm-immutable, @psalm-pure, conditional return types, and precise array-shape inference that catches a wrong key before you index it.

PHPStan has closed most of that gap and, at levels 9 and 10, pushes generic and mixed narrowing hard. The practical read in 2026: Psalm still edges ahead on exotic type gymnastics, PHPStan is close enough that most application code won't feel the difference. If you're authoring a library where the public type surface is the product, Psalm's expressiveness earns its keep.

Custom rules

You'll want project-specific checks eventually. "No env() calls outside config." "Domain classes can't import from Infrastructure." Both tools support this, with different weight.

PHPStan custom rules are a PHP class and a config line:

final class NoEnvOutsideConfigRule implements Rule
{
    public function getNodeType(): string
    {
        return FuncCall::class;
    }

    public function processNode(
        Node $node,
        Scope $scope,
    ): array {
        // inspect the node, return errors
        return [];
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it in phpstan.neon under rules: and you're done. The interface is small and the examples are everywhere.

Psalm uses a plugin system with event hooks (AfterExpressionAnalysisInterface and friends). It can do more — it hooks deeper into the analysis passes — but there's more scaffolding to stand up your first plugin.

For a team that writes a handful of architecture-fitness rules, PHPStan's model gets you there with less ceremony. For a tool vendor building something elaborate, Psalm's hooks give more room.

The security angle Psalm owns

Here's the one capability that isn't a matter of degree. Psalm ships taint analysis.

vendor/bin/psalm --taint-analysis
Enter fullscreen mode Exit fullscreen mode

It traces untrusted input — $_GET, $_POST, request bodies — from source to dangerous sink, and flags the path where user data reaches a SQL string or an unescaped echo.

// Psalm taint analysis flags this: $_GET flows
// into a query with no escaping (SQL injection).
$id = $_GET['id'];
$pdo->query("SELECT * FROM users WHERE id = $id");
Enter fullscreen mode Exit fullscreen mode

PHPStan has no equivalent in core. Some codebases handle untrusted input at a lot of boundaries: a public API, a legacy app with raw queries, anything where injection is a live risk. There, Psalm's taint mode is a reason to pick it, or to run it alongside whatever else you use.

IDE integration

Psalm ships a language server built in:

vendor/bin/psalm --language-server
Enter fullscreen mode Exit fullscreen mode

Point VS Code or any LSP-aware editor at it and you get inline errors as you type, no plugin gymnastics. PHPStan has no official language server; you rely on your editor's integration or run it on save.

If your team lives in PhpStorm, both are first-class — the IDE runs either as an inspection and shows results inline. If your team is on VS Code or Neovim and wants type errors live in the gutter, Psalm's built-in LSP is the smoother path.

Pick by codebase

Reach for PHPStan when:

  • You're on Laravel or Symfony. Larastan, phpstan-symfony, and phpstan-doctrine are among the most mature framework extensions in the ecosystem.
  • You want the most active maintenance and fastest support for new PHP versions.
  • You have a large team and want a low-ceremony custom-rule model and a big hiring pool that already knows the tool.

Reach for Psalm when:

  • Security matters at the code level and you want taint analysis instead of hoping a scanner catches it later.
  • You're authoring a library where immutability, purity, and precise generic types are the deliverable.
  • Your editors are LSP-based and you want a built-in language server with no extra plugin.

Running both is defensible for a security-sensitive library: PHPStan in CI for general correctness, Psalm's taint pass as a dedicated security gate. For a normal application, that's more config than payoff. Pick one, set the baseline, and spend the saved time fixing the errors it finds.

The tools stopped being interchangeable a while ago. The question isn't which one is better. It's which one fits the code you actually have.


If this was useful

A static analyzer is a boundary check: it stops bad types at the edge before they reach your domain. The same instinct scales up to architecture — keep framework concerns, untrusted input, and persistence at the edge, and the core of your app stays small enough to reason about. That edge-versus-core discipline is what Decoupled PHP is about, and it's what makes either analyzer's job easier the deeper you go.

Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework

Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.

Top comments (0)