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.
Table of contents
- 1. Why Psalm vs. PHPStan is not just a matter of taste
- 2. Taint analysis: Psalm's unique feature
- 3. Immutability and purity: @psalm-immutable and @psalm-pure
- 4. Type narrowing and Psalm's type system extensions
- 5. PHPStan's strengths: ecosystem, generics maturity and baseline
- 6. Configuration compared: psalm.xml vs. phpstan.neon
- 7. Migration and running both: two tools at once?
- 8. A decision framework for teams
- 9. Psalm vs. PHPStan in direct comparison
- 10. Summary
- 11. FAQ
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?
2What is taint analysis in Psalm?
3What does @psalm-immutable do?
4Is PHPStan's generics support better than Psalm's?
5Can I use both tools at the same time?
6Which tool fits better with Symfony or Laravel?
7How do the configuration formats differ?
8How much effort is a migration from Psalm to PHPStan?
9For which projects is Psalm the better choice?
10How do I find out which tool fits my team?
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