Home PHP PHPStan: Static Analysis for PHP – Catch Bugs Before Runtime
Intermediate 3 min · July 13, 2026

PHPStan: Static Analysis for PHP – Catch Bugs Before Runtime

Learn how PHPStan catches bugs without running code.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of PHP syntax and types
  • Familiarity with Composer
  • A PHP project to analyse (optional but recommended)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • PHPStan analyzes your PHP code without executing it, detecting type mismatches, missing return types, and undefined variables.
  • It integrates with your CI pipeline to prevent bugs from reaching production.
  • Levels 0-9 allow gradual adoption; start with level 0 and increase as your codebase improves.
  • Use phpstan.neon config to customize rules, exclude paths, and define stub files.
  • Common mistakes: ignoring baseline, not updating after refactoring, and misconfiguring autoload.
✦ Definition~90s read
What is PHPStan?

PHPStan is a static analysis tool that inspects your PHP code for errors without executing it, helping you catch bugs early in development.

Imagine you're building a house.
Plain-English First

Imagine you're building a house. PHPStan is like a building inspector who checks the blueprints before any construction starts. They spot missing support beams (missing return types) or wrong materials (type mismatches) so you fix them on paper, not after the walls are up. This saves time, money, and prevents collapses in production.

You've just deployed a new feature. Hours later, your monitoring alerts spike with a 500 error. The culprit? A function expected an array but got null. This is the classic 'undefined array key' or 'call to a member function on null' that haunts PHP developers. While PHP's dynamic nature is flexible, it's also a breeding ground for runtime surprises.

Enter PHPStan: a static analysis tool that inspects your code without executing it. It's like having a senior developer review every line of your codebase, catching type errors, missing return types, and dead code before you even run a test. PHPStan operates on levels 0 to 9, where 0 is the most lenient and 9 is the strictest. You can start at a low level and gradually increase as your code improves.

In this tutorial, you'll learn how to install PHPStan, configure it for your project, and integrate it into your development workflow. We'll cover real-world scenarios like handling legacy code with baselines, custom rules, and debugging production incidents. By the end, you'll have a robust static analysis setup that prevents bugs from reaching users.

PHPStan is not just a linter; it's a powerful tool that understands PHP's type system, including generics (via PHPDoc), union types, and even frameworks like Laravel. It can detect issues like accessing undefined array keys, calling methods on nullable objects, and mismatched type hints. This proactive approach saves countless hours of debugging and improves code quality across your team.

Installing PHPStan

PHPStan is installed via Composer. You can install it globally or per-project. For most projects, per-project installation is recommended to avoid version conflicts.

``bash composer require --dev phpstan/phpstan ``

This installs the latest stable version. If you need bleeding-edge features, you can use phpstan/phpstan:dev-master, but be cautious.

``bash vendor/bin/phpstan --version ``

You should see output like PHPStan - PHP Static Analysis Tool 1.12.x. Now you're ready to analyse your code.

install.phpPHP
1
2
3
4
<?php
// No code needed for installation, but here's a simple script to test:
echo "PHPStan installed successfully!";
Output
PHPStan installed successfully!
💡Global Installation
📊 Production Insight
In CI, always use a specific version of PHPStan to avoid unexpected rule changes. Pin it in your composer.json.
🎯 Key Takeaway
Install PHPStan per project using Composer to keep versions consistent across your team.

Running Your First Analysis

Let's run PHPStan on a simple PHP file. Create a file test.php with a deliberate type error:

```php <?php

function add(int $a, int $b): int { return $a + $b; }

echo add('hello', 5); // Error: string passed instead of int ```

``bash vendor/bin/phpstan analyse test.php --level=0 ``

`` ------ ------------------------------------------------------- Line test.php ------ ------------------------------------------------------- 7 Parameter #1 $a of function add expects int, string given. ------ ------------------------------------------------------- ``

PHPStan caught the type mismatch without executing the code. The --level flag sets the strictness. Level 0 is the most basic; level 9 is the strictest. Start low and increase gradually.

test.phpPHP
1
2
3
4
5
6
7
8
<?php

function add(int $a, int $b): int {
    return $a + $b;
}

echo add('hello', 5);
Output
Parameter #1 $a of function add expects int, string given.
🔥Levels Explained
📊 Production Insight
In CI, start with level 0 and a baseline to ignore existing errors. Gradually increase the level as you fix issues.
🎯 Key Takeaway
Run PHPStan with --level to set strictness. Start at level 0 and increase as your code improves.

Configuring PHPStan with phpstan.neon

For real projects, you'll want a configuration file. Create phpstan.neon in your project root:

``neon parameters: level: 6 paths: - src excludePaths: - src/Legacy checkMissingIterableValueType: true checkGenericClassInNonGenericObjectType: true ``

``bash vendor/bin/phpstan analyse ``

PHPStan automatically reads phpstan.neon. You can also specify a custom config with -c.

Key configuration options
  • level: analysis strictness (0-9)
  • paths: directories to analyse
  • excludePaths: directories to skip (e.g., legacy code, vendor)
  • checkMissingIterableValueType: ensures arrays have type hints
  • checkGenericClassInNonGenericObjectType: checks for proper generic usage

You can also define stub files for classes that PHPStan can't autoload (e.g., C extensions).

phpstan.neonPHP
1
2
3
4
5
6
7
8
9
parameters:
    level: 6
    paths:
        - src
    excludePaths:
        - src/Legacy
    checkMissingIterableValueType: true
    checkGenericClassInNonGenericObjectType: true
⚠ Exclude Paths Carefully
📊 Production Insight
In CI, commit the phpstan.neon file. Use environment-specific configs (e.g., phpstan.ci.neon) for stricter rules.
🎯 Key Takeaway
Use phpstan.neon to configure analysis paths, level, and additional checks. Exclude legacy code but prefer baselines.

Working with Baselines

When introducing PHPStan to an existing codebase, you'll likely have hundreds of errors. Instead of fixing them all at once, generate a baseline:

``bash vendor/bin/phpstan analyse --generate-baseline ``

This creates a phpstan-baseline.neon file listing all current errors. PHPStan will ignore them in future runs, so you can focus on new errors. Over time, fix baseline errors and regenerate the baseline to keep it clean.

``neon includes: - phpstan-baseline.neon ``

Now, when you run PHPStan, only new errors will be reported. This is essential for gradual adoption.

phpstan-baseline.neonPHP
1
2
3
4
5
6
7
8
9
parameters:
    ignoreErrors:
        -
            message: '#^Call to an undefined method .+::oldMethod\(\)\.$#'
            path: src/Legacy/OldClass.php
        -
            message: '#^Parameter \#1 \$x of function expects int, string given\.$#'
            path: src/Legacy/AnotherClass.php
💡Regenerate Baselines
📊 Production Insight
In CI, fail the build if the baseline changes unexpectedly (e.g., due to a dependency update). Use --generate-baseline only on purpose.
🎯 Key Takeaway
Baselines allow incremental adoption. Generate a baseline, fix errors over time, and regenerate to track progress.

Custom Rules and Extensions

PHPStan supports custom rules via extensions. You can create your own rule by implementing the PHPStan\Rules\Rule interface. For example, a rule that forbids using var_dump in production code:

```php <?php

use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder;

class NoVarDumpRule implements Rule { public function getNodeType(): string { return Node\Expr\FuncCall::class; }

public function processNode(Node $node, Scope $scope): array { if ($node->name instanceof Node\Name && $node->name->toString() === 'var_dump') { return [ RuleErrorBuilder::message('Do not use var_dump in production code.') ->line($node->getLine()) ->build(), ]; } return []; } } ```

``neon services: - class: NoVarDumpRule tags: - phpstan.rules.rule ``

Now PHPStan will flag any var_dump calls.

NoVarDumpRule.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

class NoVarDumpRule implements Rule
{
    public function getNodeType(): string
    {
        return Node\Expr\FuncCall::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if ($node->name instanceof Node\Name && $node->name->toString() === 'var_dump') {
            return [
                RuleErrorBuilder::message('Do not use var_dump in production code.')
                    ->line($node->getLine())
                    ->build(),
            ];
        }
        return [];
    }
}
🔥Extension Ecosystem
📊 Production Insight
Use custom rules to enforce security practices, like banning dangerous functions or ensuring proper input validation.
🎯 Key Takeaway
Create custom rules to enforce project-specific coding standards. Register them in phpstan.neon.

Integrating PHPStan into CI/CD

To prevent bugs from reaching production, run PHPStan in your CI pipeline. Here's an example for GitHub Actions:

```yaml name: PHPStan

on: [push, pull_request]

jobs: phpstan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.2' tools: composer - run: composer install --no-progress - run: vendor/bin/phpstan analyse --no-progress --error-format=github ```

This will fail the build if PHPStan finds errors. Use --error-format=github for annotations in pull requests.

``yaml phpstan: stage: test script: - composer install - vendor/bin/phpstan analyse --no-progress ``

Always use --no-progress in CI to avoid log clutter.

.github/workflows/phpstan.ymlPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name: PHPStan

on: [push, pull_request]

jobs:
  phpstan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          tools: composer
      - run: composer install --no-progress
      - run: vendor/bin/phpstan analyse --no-progress --error-format=github
⚠ Memory Limits
📊 Production Insight
Use a separate CI job for PHPStan to run in parallel with tests. Fail fast on static analysis errors.
🎯 Key Takeaway
Integrate PHPStan into CI to catch errors automatically. Use --error-format=github for PR annotations.
● Production incidentPOST-MORTEMseverity: high

The Null That Broke the Checkout

Symptom
Users saw '500 Internal Server Error' when clicking 'Place Order' during checkout.
Assumption
The developer assumed the payment gateway always returns a response object.
Root cause
The payment gateway returned null on timeout, but the code called $response->isSuccess() without checking for null.
Fix
Added a null check and a fallback error message. Also configured PHPStan to enforce strict checks on nullable returns.
Key lesson
  • Always check for null when calling external services, even if documentation says otherwise.
  • Use PHPStan's strict rules to flag potentially null values before runtime.
  • Implement a baseline for legacy code and gradually fix issues.
  • Integrate PHPStan into CI to catch regressions automatically.
  • Treat static analysis warnings as potential production bugs.
Production debug guideSymptom to Action4 entries
Symptom · 01
PHPStan reports 'Call to an undefined method' but the method exists.
Fix
Check if the class is imported correctly. Use use statements and verify autoloading. Add a stub file if the method is from a dynamic source.
Symptom · 02
PHPStan says 'Parameter #1 expects string, int given' but you pass a string.
Fix
Check the variable type at the call site. Use var_dump or gettype to confirm. The issue might be a type mismatch from a previous function.
Symptom · 03
PHPStan reports 'Cannot call method on mixed'.
Fix
Add type hints to the variable. Use assert() or instanceof to narrow the type. Alternatively, use PHPDoc @var to specify the expected type.
Symptom · 04
PHPStan ignores errors that should be caught.
Fix
Check your phpstan.neon configuration. Ensure the level is high enough and that you haven't excluded the path. Also verify that the baseline is up-to-date.
★ Quick Debug Cheat SheetCommon PHPStan errors and immediate fixes.
Call to undefined method on nullable
Immediate action
Add null check before method call
Commands
phpstan analyse --level=6 src/
phpstan analyse --generate-baseline
Fix now
if ($obj !== null) { $obj->method(); }
Parameter type mismatch+
Immediate action
Add type hint or cast
Commands
phpstan analyse src/ --debug
phpstan analyse --memory-limit=1G
Fix now
function foo(string $bar) { ... }
Undefined variable+
Immediate action
Initialize variable before use
Commands
phpstan analyse --level=0 src/
phpstan analyse --pro
Fix now
$var = null; // or default value
FeaturePHPStanPsalm
Level System0-91-8 (with Taint analysis)
Taint AnalysisVia extensionsBuilt-in
BaselineYesYes
Custom RulesYesYes
Laravel SupportLarastan extensionPsalm Laravel plugin
PerformanceFastModerate
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
install.phpecho "PHPStan installed successfully!";Installing PHPStan
test.phpfunction add(int $a, int $b): int {Running Your First Analysis
phpstan.neonparameters:Configuring PHPStan with phpstan.neon
phpstan-baseline.neonparameters:Working with Baselines
NoVarDumpRule.phpuse PhpParser\Node;Custom Rules and Extensions
.githubworkflowsphpstan.ymlname: PHPStanIntegrating PHPStan into CI/CD

Key takeaways

1
PHPStan catches type errors and undefined variables without running code, preventing runtime bugs.
2
Use baselines for incremental adoption in legacy projects.
3
Integrate PHPStan into CI to enforce code quality automatically.
4
Custom rules allow enforcing project-specific standards and security practices.

Common mistakes to avoid

3 patterns
×

Not using a baseline when adopting PHPStan on a large codebase.

×

Setting the level too high too early.

×

Ignoring PHPStan warnings because 'it works in production'.

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is static analysis and how does PHPStan help?
Q02SENIOR
Explain the PHPStan level system. How would you introduce it to a legacy...
Q03SENIOR
How would you create a custom PHPStan rule to forbid using `eval()`?
Q01 of 03JUNIOR

What is static analysis and how does PHPStan help?

ANSWER
Static analysis examines code without executing it. PHPStan catches type errors, undefined variables, and mismatched return types before runtime, reducing bugs and improving code quality.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between PHPStan and Psalm?
02
Can PHPStan detect security vulnerabilities?
03
How do I handle dynamic method calls?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's Advanced PHP. Mark it forged?

3 min read · try the examples if you haven't

Previous
Doctrine ORM in Symfony
23 / 29 · Advanced PHP
Next
Pest PHP: Modern Testing Framework