Psalm vs. PHPStan: Differences in Analysis Depth and Ecosystem
AI generated
<?php
8.4
PHP · Static Analysis · Psalm · PHPStan
Psalm vs. PHPStan
Differences in analysis depth and ecosystem

Psalm vs. PHPStan is rarely a question of right or wrong, but of priorities: taint analysis and aggressive type narrowing speak for Psalm, a larger plugin ecosystem and mature generics support speak for PHPStan. This article compares both tools technically and provides a practical decision framework for teams.

16 min read Taint Analysis · Generics · Ecosystem · Decision Help PHP 8.4 · Composer · CI/CD

1. Why Psalm vs. PHPStan is not just a matter of taste

Both tools read the same PHP code and the same PHPDoc dialect, both detect wrong argument types, impossible comparisons and risky null access. Anyone who only compares a feature list quickly concludes both tools are interchangeable. With Psalm vs. PHPStan, however, a closer look at analysis depth in specialized areas and at the surrounding ecosystem pays off, because that is exactly where the two tools differ noticeably in practice.

Psalm was developed by Vimeo with a clear focus on type safety and security analysis and comes with a taint analysis engine that PHPStan simply does not have at its core. PHPStan, developed by Ondřej Mirtes, has become the de facto standard in many Symfony and Laravel adjacent projects thanks to a very active community and a broad ecosystem of extensions for frameworks, test libraries and special cases. Both projects are open source, both are actively developed, both support PHP 8.4 with the current language features.

The following sections work out the concrete differences between Psalm vs. PHPStan, from security analysis through type system nuances to the practical question of which tool suits which team, and whether running both tools in parallel even makes sense.

2. Taint analysis: Psalm's unique feature in the Psalm vs. PHPStan comparison

Taint analysis tracks how unsafe input, for example from $_GET, $_POST or external APIs, flows through the code until it lands in a dangerous spot, for example an SQL query, an echo call without escaping, or a shell command. Psalm internally marks such input as "tainted" and reports an error as soon as such a value reaches a sensitive sink unfiltered, without a recognized sanitizing function in between. This is a qualitatively different type of analysis than pure type checking, it tracks the data flow across multiple function calls.


<?php

declare(strict_types=1);

namespace App\Http;

final class SearchController
{
    public function __construct(private readonly \PDO $pdo)
    {
    }

    public function search(): void
    {
        // Psalm's taint analysis flags this: user input flows
        // directly into a raw SQL string without a sanitizer.
        $term = $_GET['q'] ?? '';
        $stmt = $this->pdo->query("SELECT * FROM products WHERE name LIKE '%{$term}%'");

        foreach ($stmt->fetchAll() as $row) {
            // Also flagged: unescaped output of tainted data (XSS sink).
            echo $row['name'];
        }
    }
}

PHPStan has no comparable built-in taint engine, it checks types and control flow, but not whether a string variable originally came from an untrusted source. For projects with high security requirements, for example publicly reachable APIs or forms with database access, this is a decisive difference in Psalm vs. PHPStan: Psalm can find SQL injection and XSS patterns that pure type checking fundamentally cannot detect, because the data type itself, for example string, remains unchanged, only its origin is relevant.

In practice you enable taint analysis via taintAnalysis="true" in psalm.xml and declare custom sinks and sources via @psalm-taint-sink and @psalm-taint-source, in case project-specific functions such as a custom database wrapper are not captured by automatic detection. The effort pays off especially in codebases that process direct user input, less so in pure batch or CLI applications without an external attack surface.

3. Immutability and purity: @psalm-immutable and @psalm-pure

Psalm comes with its own set of annotations for functional properties that PHPStan does not know at its core. @psalm-immutable marks a class as immutable, Psalm then statically verifies that no method changes a property value after construction, and reports an error as soon as such a class contains a setter or a direct assignment outside the constructor after all. @psalm-pure marks a function as side-effect free, its return value depends exclusively on its arguments, no global state, no I/O, no static access.


<?php

declare(strict_types=1);

namespace App\ValueObject;

/**
 * @psalm-immutable
 */
final class Money
{
    public function __construct(
        private readonly int $cents,
        private readonly string $currency,
    ) {
    }

    /**
     * @psalm-pure
     */
    public function add(self $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new \InvalidArgumentException('Currency mismatch');
        }

        // Psalm verifies this returns a new instance without mutating $this.
        return new self($this->cents + $other->cents, $this->currency);
    }
}

The benefit of these annotations lies in the fact that Psalm exploits them in further analyses: a call marked as @psalm-pure may appear multiple times in the same expression without side effects being a concern, and Psalm can draw additional optimization and correctness conclusions from that, for example when comparing two calls with identical arguments. PHPStan only knows immutability indirectly through readonly properties at the language level since PHP 8.1, but there is no comparable annotation that declares an entire class or function as pure and actively verifies this against the method body.

For value objects, DTOs and functionally oriented code, this is a noticeable difference in expressiveness in Psalm vs. PHPStan: anyone who consistently works with immutable objects gets an additional, statically verified guarantee with Psalm that this immutability is actually upheld throughout the entire codebase, instead of relying only on convention and code review.

4. Type narrowing and Psalm's type system extensions

Type narrowing refers to an analyzer's ability to restrict the type of a variable within a code block based on preceding checks, for example after an instanceof or an is_string() check. Psalm is traditionally considered more aggressive here: it tracks type information across more complex control flows, for example after an early return in a condition, after assignments in loops, or across multiple nested if conditions with logical operators.

In addition, Psalm offers its own type system extensions that go beyond the PHPDoc standard: @psalm-assert declares on a function which type is guaranteed after a successful call, for example that a parameter is then guaranteed to no longer be null, useful for custom validation and guard functions. Conditional types allow the return type of a function to depend on the type of a parameter, a pattern that appears in generic libraries with multiple possible return types.


<?php

declare(strict_types=1);

namespace App\Assert;

final class Guard
{
    /**
     * @psalm-assert !null $value
     */
    public static function notNull(mixed $value, string $message): void
    {
        if ($value === null) {
            throw new \InvalidArgumentException($message);
        }
    }
}

// Usage: after this call, Psalm narrows $order to non-null
// for the rest of the function without an extra if-check.
function processOrder(?Order $order): void
{
    Guard::notNull($order, 'Order must not be null');
    $order->markAsShipped();
}

PHPStan meanwhile also supports assert-style annotations via @phpstan-assert, with comparable functionality for a few versions now, so this concrete difference in Psalm vs. PHPStan has narrowed in recent years. With more complex, deeply nested control flows and combinations of multiple conditions, Psalm still tends to remain more precise according to the experience of many teams, which in particular leads to fewer false positives when refactoring older, nested code.

5. PHPStan's strengths: ecosystem, generics maturity and baseline tooling

The decisive advantage of PHPStan in Psalm vs. PHPStan rarely lies in a single feature, but in the surrounding environment: for practically every widely used PHP framework there is a maintained PHPStan extension, for example phpstan-symfony, phpstan-doctrine or phpstan-mockery, which correctly incorporates framework-specific behavior such as dependency injection or ORM magic into the analysis. Comparable plugins exist for Psalm too, but the overall offering is smaller and maintenance of individual plugins varies more.

Generics via @template were developed by PHPStan and Psalm in parallel, in practice PHPStan's implementation is today considered more mature in edge cases, for example with multiple templates in the same class, with inheritance chains across several levels, or when combining generics with union types. For library authors building complex generic collections or repository abstractions, this is a practically relevant difference, because edge cases occasionally lead to false error reports in Psalm where PHPStan resolves the type correctly.

Baseline tooling exists for both tools, phpstan analyse --generate-baseline and psalm --set-baseline work on the same principle. PHPStan's level system with stages 0 to 9 plus max offers a clearer, more established ratcheting convention in the community than Psalm's error level system 1 to 8, which experience shows makes communicating a project's current strictness level easier both within a team and in job postings.

6. Configuration compared: psalm.xml vs. phpstan.neon

Both tools use a declarative configuration format, but differ noticeably in syntax and structure. Psalm uses XML with a <projectFiles> block for the analysis scope and errorLevel as a number from 1 (strictest stage) to 8 (loosest stage), a counting direction that initially runs counter to many newcomers' expectations compared to PHPStan's levels.


<?xml version="1.0"?>
<psalm
    errorLevel="2"
    resolveFromConfigFile="true"
    findUnusedBaselineEntry="true"
    findUnusedCode="false"
>
    <projectFiles>
        <directory name="src" />
        <ignoreFiles>
            <directory name="src/Legacy" />
        </ignoreFiles>
    </projectFiles>

    <taintAnalysis>
        <directory name="src/Http" />
    </taintAnalysis>
</psalm>

PHPStan instead uses NEON, a YAML-like format, with level as a number from 0 (loosest stage) to 9 or the alias max, here strictness intuitively increases with the number, which in discussions about Psalm vs. PHPStan is often mentioned as a small but noticeable advantage for the onboarding experience of new team members.


# phpstan.neon
parameters:
    level: 8
    paths:
        - src
    excludePaths:
        - src/Legacy/*
    checkMissingIterableValueType: true

Functionally, both configuration approaches are equally powerful, both support path exclusions, baseline includes and project-specific strictness parameters. The difference lies more in the familiarity of the format for the given team: anyone who already has phpunit.xml or similar XML configurations in the project finds their way around psalm.xml faster, anyone working with YAML-based CI configs finds NEON more natural.

7. Migration and running both: two tools at once?

Technically nothing prevents Psalm and PHPStan from running in parallel in the same project, both read the same source code without influencing each other, and both can be run independently via separate Composer scripts and CI jobs. In practice this pays off especially when a project specifically wants to use Psalm's taint analysis for security-critical areas, while the rest of the codebase runs under PHPStan with its larger framework ecosystem.

The cost of running both lies in the doubled maintenance of ignore lists and annotations: some PHPDoc patterns are interpreted differently by the two tools, which leads to situations where an annotation that satisfies PHPStan triggers a new error in Psalm, and vice versa. Realistically, running both proves more worthwhile in smaller, clearly bounded security modules than across an entire, large codebase.

A complete migration from one tool to the other is more effort than switching a PHPStan level, because annotations such as @psalm-immutable or @psalm-assert have no direct equivalent in PHPStan and must, in doubt, be removed or replaced with PHPStan's own alternatives such as readonly classes. With Psalm vs. PHPStan, it therefore pays off to decide as early as possible in the project, a later switch is doable, but comes with noticeable manual effort.

8. A decision framework for teams

For teams primarily operating publicly reachable endpoints, forms or APIs with direct user input and treating security analysis as a hard requirement, taint analysis clearly speaks for Psalm, a feature that cannot be replicated with PHPStan at its core. For teams primarily building on a widely used framework like Symfony or Laravel and wanting to benefit from a broad, well-maintained extension ecosystem, PHPStan is usually the more pragmatic choice.

For library authors with complex generic types, PHPStan's more mature generics implementation pays off, while functionally oriented codebases with a strict focus on immutability benefit from Psalm's @psalm-immutable and @psalm-pure annotations. In mixed teams with different preferences, experience often decides: a team already familiar with one of the two tools should not give up that head start without a solid reason, because the learning curve for a new static analysis tool is real, even though both tools share similar PHPDoc conventions.

A pragmatic middle path for undecided teams: run both tools initially at a low level or low errorLevel in parallel in a proof of concept over a limited part of the codebase, compare the number and kind of errors found, and then make a decision based on concrete, project-specific data instead of pure theory.

9. Psalm vs. PHPStan in direct comparison

The following table summarizes the most important differences between Psalm vs. PHPStan along the dimensions that most often tip the scales in practice.

Dimension Psalm PHPStan
Taint analysis Built in, SQL injection and XSS patterns Not available at the core
Immutability annotations @psalm-immutable, @psalm-pure Only via readonly properties
Generics maturity Solid, edge cases occasionally imprecise More mature with complex inheritance chains
Plugin ecosystem Smaller, varying maintenance Large, many framework extensions
Baseline tooling --set-baseline, functionally equivalent --generate-baseline, functionally equivalent
Community size Smaller, but active Considerably larger, de facto standard

No tool wins in every row of this table, and that is exactly what makes Psalm vs. PHPStan a project-specific decision rather than a universal recommendation. Security-critical applications benefit more from Psalm's taint analysis, while framework-heavy projects with many dependencies usually fare better with PHPStan's ecosystem.

10. Summary

Psalm vs. PHPStan is not a duel with a clear winner, but two different areas of emphasis within the same tool category. Psalm brings strengths with built-in taint analysis, @psalm-immutable and more aggressive type narrowing that come into their own particularly in security-critical and functionally oriented codebases. PHPStan scores with a considerably larger plugin ecosystem, more mature generics in complex cases, and a level convention more firmly established in the community.

For the practical decision, an abstract feature list matters less than the concrete project situation: security requirements, framework in use, existing team experience, and the willingness to commit to a type system dialect. A short proof of concept with both tools on a limited slice of code often delivers more solid arguments than any theoretical comparison, including this article.

Psalm vs. PHPStan - The essentials at a glance

Psalm's unique strengths

Taint analysis for SQL injection and XSS, @psalm-immutable and @psalm-pure for functional guarantees.

PHPStan's unique strengths

Large framework ecosystem, more mature generics in complex cases, established level convention.

Configuration

psalm.xml with errorLevel 1-8 versus phpstan.neon with level 0-9 or max, functionally equally powerful.

Decision help

Security-critical and functional: Psalm. Framework-heavy with many dependencies: PHPStan. Proof of concept before deciding.

11. FAQ: Psalm vs. PHPStan

1What is the most important difference in Psalm vs. PHPStan?
Psalm has built-in taint analysis for security checks, PHPStan scores with a larger plugin ecosystem for frameworks.
2What is taint analysis in Psalm?
Tracks unsafe user input through the code up to a dangerous spot such as an SQL query or unprotected output.
3What does @psalm-immutable do?
Marks a class as immutable, Psalm statically verifies no method changes property values after construction.
4Is PHPStan's generics support better than Psalm's?
With complex inheritance chains and union types PHPStan is considered somewhat more mature, in simple cases both are equivalent.
5Can I use both tools at the same time?
Technically yes, practically more useful for clearly bounded security modules, due to doubled maintenance of ignore lists.
6Which tool fits better with Symfony or Laravel?
PHPStan, thanks to maintained framework extensions like phpstan-symfony and phpstan-doctrine.
7How do the configuration formats differ?
psalm.xml with errorLevel 1-8, phpstan.neon with level 0-9 or max, functionally equally powerful.
8How much effort is a migration from Psalm to PHPStan?
More effort than a level switch, since annotations like @psalm-immutable must be replaced with alternatives.
9For which projects is Psalm the better choice?
For projects with direct user input and high security requirements, and functionally oriented code focused on immutability.
10How do I find out which tool fits my team?
A short proof of concept with both tools on a limited slice of code delivers more solid arguments than theory.

Mironsoft

Static analysis, code quality and security analysis for PHP projects

Not sure which static analysis tool fits your project?

We assess your codebase, security requirements and framework, run a proof of concept with Psalm and PHPStan, and set up the right tool cleanly in your CI pipeline.

Tool selection

Proof of concept with Psalm and PHPStan on a real slice of your project

Security analysis

Taint analysis setup for security-critical endpoints and forms

CI integration

Baseline setup and build rules, whether implemented with Psalm or PHPStan