paulund
#php #phpstan #static-analysis

PHPStan: Advanced Static Analysis for PHP

PHPStan is a static analysis tool for PHP that finds bugs in your code without running it. It performs deep analysis of your codebase to catch errors, enforce type safety, and improve code quality before anything reaches production.

The current major release is PHPStan 2.x, which shipped in late 2024. The analysis levels, configuration format, and core behaviour remain the same — though some parameter names changed and a few deprecated options were removed. If you're upgrading from 1.x, running ./vendor/bin/phpstan will surface any config issues immediately.

Table of Contents

  1. What is PHPStan?
  2. Why Use PHPStan?
  3. Installation
  4. Configuration Explained
  5. Analysis Levels
  6. Common Use Cases
  7. Running PHPStan
  8. Best Practices
  9. Advanced Configuration

What is PHPStan?

PHPStan analyses your PHP code without executing it. It:

  • Detects bugs before they reach production
  • Enforces type safety through comprehensive type checking
  • Validates code logic and identifies unreachable code
  • Analyses method calls and property access
  • Checks PHPDoc annotations for accuracy

PHPStan understands your code structure and can identify issues that traditional testing often misses, particularly around type contracts and nullability.

Why Use PHPStan?

Early Bug Detection

PHPStan catches errors before they cause runtime failures: undefined variables and methods, type mismatches, logic errors with unreachable code, and missing return statements. Finding these at analysis time is far cheaper than tracking them down in a staging or production environment.

Type Safety Enforcement

PHPStan ensures your code respects type contracts. It validates method signatures, checks parameter and return types, identifies nullable type violations, and enforces strict type declarations. With more PHP codebases adopting strict types, this is increasingly important.

Refactoring Confidence

PHPStan provides a safety net during code changes. When you modify an API or refactor a service, PHPStan will flag every call site that breaks before you run a single test. That feedback loop is genuinely useful on larger codebases.

Installation

Via Composer (Recommended)

# Install as dev dependency
composer require --dev phpstan/phpstan

# Install with the extension installer (optional but convenient)
composer require --dev phpstan/extension-installer

Verify Installation

# Check PHPStan version
./vendor/bin/phpstan --version

# Run basic analysis
./vendor/bin/phpstan analyse src

Configuration Explained

Below is an example configuration file (phpstan.neon.dist) with explanations:

parameters:
    level: max
    paths:
        - src

    reportUnmatchedIgnoredErrors: true

Configuration Breakdown

Analysis Level

level: max

Sets the strictness of analysis. The range is 0 to 9, with max always using the highest available level and including experimental rules. Start lower and work up rather than jumping straight to max on an existing codebase.

Analysis Paths

paths:
    - src

Defines which directories to analyse. Common choices are src/, app/, and tests/. Focus analysis on code you own, not generated files or vendor packages.

Error Reporting

reportUnmatchedIgnoredErrors: true

Reports when ignore patterns in your config don't match any errors. Without this, stale ignore rules quietly accumulate and can mask real problems introduced later.

Analysis Levels

PHPStan uses levels 0 to 9 to gradually increase analysis strictness.

Level 0 (Basic)

Basic syntax checking, undefined functions and classes, wrong number of arguments passed to methods.

Levels 1 to 3 (Intermediate)

Unknown methods called on objects, unknown properties accessed on objects, unknown variables in certain contexts.

Levels 4 to 6 (Advanced)

Return types declared in PHPDoc, basic dead code detection, unreachable statements after return or throw.

Levels 7 to 9 (Maximum)

Report partially wrong union types, report calling methods on nullable types, strict comparisons of incompatible types.

Level Max

Always uses the highest available level, including experimental rules. A useful future-proof setting for greenfield projects.

Common Use Cases

Type Safety Validation

// Nullable type violation — PHPStan catches this
function getName(?string $name): string
{
    return $name; // Error: might return null
}

// Correct version
function getName(?string $name): string
{
    return $name ?? 'Unknown';
}

Method and Property Validation

class User
{
    private string $name;

    public function getName(): string
    {
        return $this->name;
    }
}

$user = new User();
echo $user->email;          // Error: Property User::$email does not exist
$user->invalidMethod();     // Error: Method does not exist

Array and Collection Analysis

// Array key existence
$config = ['database' => ['host' => 'localhost']];
echo $config['cache']['driver']; // Error: Offset 'cache' does not exist

// Collection type checking
/** @var User[] $users */
$users = getUsers();
foreach ($users as $user) {
    echo $user->name; // PHPStan knows $user is a User instance
}

Return Type Validation

function findUser(int $id): User
{
    $user = User::find($id);

    if (!$user) {
        return null; // Error: Cannot return null, expects User
    }

    return $user;
}

// Correct version
function findUser(int $id): ?User
{
    return User::find($id);
}

Running PHPStan

Basic Analysis

# Analyse src directory
./vendor/bin/phpstan analyse src

# Analyse multiple directories
./vendor/bin/phpstan analyse src tests

# Analyse specific file
./vendor/bin/phpstan analyse src/User.php

Configuration Options

# Use custom configuration
./vendor/bin/phpstan analyse -c phpstan.neon

# Set analysis level
./vendor/bin/phpstan analyse --level=5 src

# Memory limit for large projects
./vendor/bin/phpstan analyse --memory-limit=1G src

Output Formats

# Default output
./vendor/bin/phpstan analyse src

# JSON output for CI/CD
./vendor/bin/phpstan analyse --error-format=json src

# GitHub Actions format
./vendor/bin/phpstan analyse --error-format=github src

Advanced Options

# Generate baseline (ignore existing errors)
./vendor/bin/phpstan analyse --generate-baseline

# Clear cache
./vendor/bin/phpstan clear-cache

# Debug mode
./vendor/bin/phpstan analyse --debug src

Best Practices

Incremental Adoption

Start with lower levels and work up gradually. Jumping to max on an existing codebase will produce hundreds of errors and make the tool feel unusable. Start at level 0, fix errors, then increment.

./vendor/bin/phpstan analyse --level=0 src
./vendor/bin/phpstan analyse --level=5 src
./vendor/bin/phpstan analyse --level=max src

Use Baseline for Legacy Code

If you're adding PHPStan to an existing project, generate a baseline first. This ignores pre-existing errors so you can enforce the tool going forward without being buried immediately.

# Generate baseline
./vendor/bin/phpstan analyse --generate-baseline

# Subsequent runs ignore baseline errors
./vendor/bin/phpstan analyse

CI/CD Integration

Add PHPStan to your CI pipeline so analysis runs on every pull request. Use the github error format so errors appear inline in the PR diff.

# .github/workflows/phpstan.yml
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.3"
            - run: composer install --optimize-autoloader
            - run: ./vendor/bin/phpstan analyse --error-format=github

IDE Integration

VS Code with PHPStan extension:

{
    "phpstan.enabled": true,
    "phpstan.path": "./vendor/bin/phpstan",
    "phpstan.configFile": "phpstan.neon"
}

Real-time feedback in the editor catches issues before you even commit. Worth setting up if you spend time in VS Code.

Advanced Configuration

Complete Configuration Example

parameters:
    level: max
    paths:
        - src
        - tests

    # Exclude specific directories
    excludePaths:
        - src/Legacy/*
        - tests/fixtures/*

    # Custom error reporting
    reportUnmatchedIgnoredErrors: true
    checkMissingIterableValueType: true
    checkGenericClassInNonGenericObjectType: true

    # Ignore specific errors
    ignoreErrors:
        - '#Call to an undefined method App\\User::invalidMethod\(\)#'
        -
            message: '#Access to an undefined property#'
            path: src/Legacy/OldClass.php

    # Additional rules
    checkAlwaysTrueCheckTypeFunctionCall: true
    checkAlwaysTrueInstanceof: true
    checkAlwaysTrueStrictComparison: true
    checkExplicitMixedMissingReturn: true
    checkFunctionNameCase: true
    checkInternalClassCaseSensitivity: true

    # Bootstrap files
    bootstrapFiles:
        - tests/bootstrap.php

    # Autoload directories
    scanDirectories:
        - src/helpers

    # Stub files for missing extensions
    stubFiles:
        - stubs/custom.stub

Framework-Specific Configuration

Laravel with Larastan v3:

Larastan v3 targets PHPStan 2.x. Install it as a dev dependency and include the extension in your config.

composer require --dev larastan/larastan
includes:
    - ./vendor/larastan/larastan/extension.neon

parameters:
    level: max
    paths:
        - app
        - database
        - routes
        - tests

    # Laravel-specific ignores
    ignoreErrors:
        - '#Unsafe usage of new static\(\)#'
        - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder#'

    # Laravel features
    checkMissingIterableValueType: false
    checkGenericClassInNonGenericObjectType: false

Symfony Configuration:

includes:
    - ./vendor/phpstan/phpstan-symfony/extension.neon

parameters:
    level: max
    paths:
        - src
        - tests

    symfony:
        container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml

Custom Rules and Extensions

parameters:
    level: max
    paths:
        - src

    # Custom rules
    rules:
        - App\PHPStan\Rules\NoEntityManagerInControllerRule

    # Service extensions
    services:
        -
            class: App\PHPStan\Extension\CustomExtension
            tags:
                - phpstan.broker.dynamicMethodReturnTypeExtension

    # Type extensions
    typeAliases:
        UserId: 'int<1, max>'
        EmailAddress: 'string'

Performance Optimisation

parameters:
    level: max
    paths:
        - src

    # Parallel processing
    parallel:
        jobSize: 20
        maximumNumberOfProcesses: 4

    # Cache location
    tmpDir: var/cache/phpstan

    # Exclude large generated files
    excludePaths:
        - src/Generated/*
        - '*/large-file.php'

Extensions and Integrations

Popular Extensions

# Doctrine extension
composer require --dev phpstan/phpstan-doctrine

# Symfony extension
composer require --dev phpstan/phpstan-symfony

# PHPUnit extension
composer require --dev phpstan/phpstan-phpunit

# Laravel extension (Larastan v3, requires PHPStan 2.x)
composer require --dev larastan/larastan

Integration with Other Tools

With Rector:

#!/bin/bash
./vendor/bin/rector process --dry-run
./vendor/bin/phpstan analyse
./vendor/bin/pint --test

With PHPUnit:

#!/bin/bash
./vendor/bin/phpunit
./vendor/bin/phpstan analyse

Troubleshooting

Common Issues

Memory Limit Exceeded

php -d memory_limit=1G ./vendor/bin/phpstan analyse src

Autoloading Issues

parameters:
    bootstrapFiles:
        - vendor/autoload.php
        - bootstrap/app.php

False Positives

parameters:
    ignoreErrors:
        - '#Specific error pattern#'

Performance Issues

# Clear cache
./vendor/bin/phpstan clear-cache

# Suppress progress bar for cleaner output
./vendor/bin/phpstan analyse --no-progress src

Debugging Tips

# Verbose output
./vendor/bin/phpstan analyse --debug src

# Analyse a specific file
./vendor/bin/phpstan analyse --debug src/User.php

# Generate a named baseline for debugging
./vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon

Conclusion

PHPStan is worth adding to any PHP project that's going to be maintained for more than a few months. The upfront cost of fixing errors is usually a few hours; the ongoing benefit is catching bugs before they ship.

If you're on an existing codebase, start with a baseline and level 0. Pick a level you can keep clean, fix the errors at that level, then increment. The baseline approach means you don't have to fix everything at once, and it prevents the tool from feeling like a burden.

For greenfield projects, set level: max from day one. It's much easier to maintain than to retrofit later.


Related Resources