Skip to main content
WP HealthKit

PHPStan for WordPress: Complete Static Analysis Guide

March 21, 202613 min readQualityBy Jamie

Static type analysis caught a critical bug in your codebase yesterday. You didn't notice because the code path rarely executes, and your test coverage doesn't reach it. But a user running a specific combination of plugins discovered it in production. This is where PHPStan enters the picture. PHPStan is a static analysis tool that inspects your PHP code without executing it, catching type errors, undefined methods, null pointer exceptions, and countless logical bugs before they reach users. For WordPress plugins and themes where every update reaches thousands of installations, PHPStan WordPress static analysis isn't optional—it's essential infrastructure.

This guide walks you through why static analysis matters for WordPress, how to install and configure PHPStan for your codebase, which rule levels make sense for your project, and how to integrate static analysis into your development workflow. We'll show you real bugs PHPStan catches, explain how to configure it for WordPress-specific code patterns, and demonstrate how tools like WP HealthKit automate the process so your team can focus on shipping features instead of debugging type errors.

Table of Contents

  1. What is PHPStan and Why It Matters
  2. Static Analysis vs. Other Quality Tools
  3. Installation and Initial Setup
  4. Understanding PHPStan Rule Levels
  5. Configuring PHPStan for WordPress
  6. Real Bugs PHPStan Catches
  7. Integration and CI/CD
  8. Baseline Files and Gradual Adoption
  9. Frequently Asked Questions

What is PHPStan and Why It Matters

PHPStan is a static analysis tool for PHP that doesn't require execution. It reads your source code, understands type annotations and inference, and reports potential bugs without running a single line of code. Static analysis catches entire categories of bugs that unit tests and integration tests miss—type mismatches, undefined methods, null pointer exceptions, and logical errors in branches that rarely execute.

How Static Analysis Works

PHPStan parses your PHP code into an abstract syntax tree (AST), analyzes data flow, tracks variable types throughout your codebase, and reports violations against configured rule levels. If you pass a string where a function expects an integer, PHPStan catches it. If you call a method that doesn't exist on an object, PHPStan reports it. If a variable might be null and you access a property without checking, PHPStan warns you. All of this happens without executing your code.

Why WordPress Developers Need Static Analysis

WordPress is dynamic. Core APIs accept mixed types, many functions are loosely typed, and dynamic function calls and filters make traditional type checking challenging. This dynamism is powerful but creates vulnerabilities: it's easy to pass the wrong type to a function, call methods that don't exist, or assume variables aren't null when they might be.

Static analysis tools like PHPStan bridge this gap. They understand WordPress conventions, work with dynamic patterns, and catch bugs at the source. A team running PHPStan on every commit catches type-related bugs 90% faster than finding them through QA or user reports.

The Cost of Type Errors

Consider this scenario: a WordPress plugin updates an option from a string to an array. Existing code elsewhere in the plugin still treats the option as a string. The code works fine until a user upgrades and the new code path executes, causing a runtime error and plugin failure. PHPStan catches this immediately because it sees the type mismatch across your codebase.

Static Analysis vs. Other Quality Tools

PHPStan isn't the only quality tool in your toolkit. Understanding how it complements other tools helps you build a comprehensive quality system.

PHPStan vs. PHPCS

PHPCS (PHP_CodeSniffer) with WordPress Coding Standards enforces code formatting and style conventions. PHPStan analyzes type correctness and logical bugs. They're complementary, not competitive. PHPCS says "variable names should be snake_case." PHPStan says "this variable might be null, but you're not checking for it." Run both. Together, they catch style issues and type errors.

PHPStan vs. Unit Tests

Unit tests verify behavior by executing code. Static analysis understands types without execution. A well-written unit test might miss a type error if the test doesn't exercise that code path. PHPStan catches it immediately. However, PHPStan can't verify business logic—only type correctness and existence of methods. Use both. Tests verify behavior; static analysis prevents type errors before they're tested.

PHPStan vs. Runtime Error Handlers

Tools like Sentry catch errors in production. PHPStan prevents errors from reaching production in the first place. Sentry is reactive (you know about bugs after they affect users). PHPStan is proactive (you prevent bugs before release). Combine them: use PHPStan to prevent errors and Sentry to catch what slips through.

Installation and Initial Setup

Installing PHPStan takes five minutes with Composer. You'll need PHP 7.4+ and Composer.

Step 1: Add PHPStan via Composer

composer require --dev phpstan/phpstan

This adds PHPStan to your development dependencies.

Step 2: Verify Installation

vendor/bin/phpstan --version

You should see output like "PHPStan 1.10.0".

Step 3: Create a phpstan.neon Configuration File

PHPStan behavior is configured in a phpstan.neon file at your project root:

parameters:
    level: 5
    paths:
        - src
        - inc
    excludePaths:
        - vendor
        - tests

This minimal configuration scans the src and inc directories at rule level 5.

Step 4: Run Your First Scan

vendor/bin/phpstan analyse

This scans all configured paths and reports violations.

Understanding PHPStan Rule Levels

PHPStan defines ten rule levels, from 0 (least strict) to 9 (most strict). Each level adds checks and assumes more about your code's type correctness.

Level 0: Basic Checks

At level 0, PHPStan only reports the most obvious errors: calling methods on non-objects, accessing undefined properties on objects with explicit type information, and some basic undefined variable checks. Use this when starting static analysis on a mature codebase with minimal type hints.

Level 1: Basic Class Rules

Level 1 adds checks for undefined properties on typed classes and reports when you access properties that don't exist. It also checks method existence on typed classes. Example: calling $user->fullname() when the User class only has full_name().

Level 2: Stricter Rules

Level 2 enforces stricter type rules: checking return types against function definitions, verifying argument types match parameters, and ensuring variables are used after assignment. Example: passing a string to a function expecting an integer.

Level 3: Type Hints Everywhere

Level 3 expects return types and parameter types on most functions. It reports "implicitly mixed" where types are unclear. Example: calling a function with no return type annotation and trying to use the result as a string without explicit casting.

Level 4: Advanced Type Inference

Level 4 performs advanced data flow analysis, tracking how types change through your code, and requires explicit handling of null values. Example: a variable might be null, but you're accessing a property without checking.

Level 5: Strict Null Handling (Default)

Level 5 enforces strict null checking—variables that might be null must be explicitly checked before use. Example: $user = get_user(); $user->id; fails if get_user() might return null, because you didn't check for null first.

Levels 6-9: Maximum Strictness

Levels 6 through 9 progressively require complete type declarations on properties and parameters, enforce total type coverage, restrict dynamic function calls, and treat all edge cases pessimistically.

For new projects, start at level 5—it catches most real bugs without requiring extensive type annotations. Mature projects should begin at level 2 or 3, then gradually increase as you add type hints. Legacy plugins should start at level 0 or 1, incrementally adopting higher levels using baseline files.

Configuring PHPStan for WordPress

WordPress-specific configuration requires stubs (type information for WordPress functions) and extensions to handle WordPress patterns.

Adding WordPress Stubs

WordPress functions aren't typed in core, but the community maintains type stubs. Install them:

composer require --dev php-stubs/wordpress-stubs

Then configure PHPStan to use them:

parameters:
    level: 5
    paths:
        - src
        - inc
        - functions.php
    excludePaths:
        - vendor
        - tests
    stubFiles:
        - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php

WordPress-Specific Extensions

The szepeviktor/phpstan-wordpress extension extends PHPStan with WordPress-specific rules:

composer require --dev szepeviktor/phpstan-wordpress

Then extend your configuration:

includes:
    - vendor/szepeviktor/phpstan-wordpress/extension.neon

parameters:
    level: 5
    paths:
        - src
        - inc
    excludePaths:
        - vendor
        - tests

This extension adds rules for proper use of WordPress actions and filters, recognizes common WordPress patterns, and reduces false positives.

Complete WordPress Configuration

Here's a production-ready phpstan.neon for WordPress plugins:

includes:
    - vendor/szepeviktor/phpstan-wordpress/extension.neon

parameters:
    level: 5

    paths:
        - src
        - inc
        - plugin.php

    excludePaths:
        - vendor/
        - tests/
        - node_modules/
        - build/

    stubFiles:
        - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php

    checkMissingCallableSignature: true
    checkMissingIterableValueType: false

    ignoreErrors:
        - identifier: "class.notFound"
          path: "**/wp-admin/includes/*"

    typeAliases:
        WpPost: 'WP_Post|null'
        WpUser: 'WP_User|null'

Real Bugs PHPStan Catches

Concrete examples illustrate PHPStan's power. Here are bugs that PHPStan would catch immediately.

Bug 1: Undefined Method on Typed Class

class UserRepository {
    public function find( int $id ): User {
        return new User( $id );
    }
}

class User {
    public function get_name(): string {
        return 'John';
    }
}

$repo = new UserRepository();
$user = $repo->find( 1 );
echo $user->get_full_name();  // PHPStan error: method doesn't exist

PHPStan reports: "Call to an undefined method User::get_full_name()." Fix: use the correct method name get_name().

Bug 2: Type Mismatch in Function Call

function update_user_age( int $age ): bool {
    return $age > 0 && $age < 150;
}

$user_input = $_POST['age'];  // string
$result = update_user_age( $user_input );  // PHPStan error

PHPStan reports: "Parameter #1 $age of function update_user_age() expects int, string given." Fix: cast the input update_user_age( (int) $user_input ).

Bug 3: Null Safety Violation

function get_user( int $id ): ?User {
    if ( $id < 1 ) {
        return null;
    }
    return new User( $id );
}

$user = get_user( 0 );
echo $user->name;  // PHPStan error: possibly null

PHPStan reports: "Cannot access property $name on User|null." Fix: check for null first:

$user = get_user( 0 );
if ( $user !== null ) {
    echo $user->name;
}

Bug 4: Wrong Type in Array

/** @var array<int, string> */
$items = [];
$items[] = 123;  // PHPStan error: int, not string

Bug 5: Missing Return Type

function calculate_total( $items ) {
    return array_sum( $items );
}

$total = calculate_total( [ 1, 2, 3 ] );
echo $total->format();  // PHPStan error at level 3+

At level 3+, PHPStan complains that you're calling a method on a variable with implicitly mixed type. Fix: add explicit return type:

function calculate_total( array $items ): int {
    return array_sum( $items );
}

Quick Audit

Wondering how many type errors are hiding in your plugin? PHPStan is just the start. WP HealthKit analyzes type safety, security vulnerabilities, performance issues, and WordPress-specific problems across 17 verification layers—including static analysis, PHPCS coding standards, and Wordfence CVE cross-referencing.

Run a free audit →


Integration and CI/CD

Running PHPStan locally is helpful, but CI/CD automation ensures type checks pass before code reaches production.

Local Development Workflow

Run PHPStan before committing:

vendor/bin/phpstan analyse && git add .

Or add a Git pre-commit hook:

#!/bin/bash
# .git/hooks/pre-commit

vendor/bin/phpstan analyse

if [ $? -ne 0 ]; then
    echo "PHPStan found errors. Fix them before committing."
    exit 1
fi

GitHub Actions Integration

Add PHPStan to your GitHub Actions workflow:

name: Static Analysis

on: [push, pull_request]

jobs:
  phpstan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.0'

      - run: composer install --no-interaction --no-progress

      - run: vendor/bin/phpstan analyse

Handling Warnings and Errors

PHPStan exits with code 0 if no errors, 1 if errors exist. In strict CI pipelines, treat PHPStan failures as blocking. This ensures no code with type errors reaches production.

Baseline Files and Gradual Adoption

Adopting PHPStan on a mature codebase with thousands of violations is daunting. Baseline files let you incrementally improve.

Creating a Baseline

Generate a baseline file capturing all current violations:

vendor/bin/phpstan analyse --generate-baseline=phpstan-baseline.neon

This creates phpstan-baseline.neon listing every violation PHPStan finds. In your configuration, reference it:

includes:
    - phpstan-baseline.neon

parameters:
    level: 5

Now CI passes even with existing violations. As you fix issues, remove them from the baseline.

Gradual Level Increase

Start at level 0 or 1, then increment. Run phpstan analyse and fix or baseline violations. Then increment to level 2. This gradual approach prevents overwhelming your team with thousands of violations at once.

Tracking Progress

Monitor how many violations remain:

vendor/bin/phpstan analyse --level 5 | grep -c "^"

Log this number over time. As violations decrease, you're improving code quality. Explore our ecosystem tools for more ways to track plugin quality.

Frequently Asked Questions

How do I handle third-party libraries without type information?

Suppress errors for external libraries or install stub packages:

parameters:
    ignoreErrors:
        - identifier: "class.notFound"
          path: "**/vendor/*"

Can PHPStan work with dynamic WordPress code?

Yes. The szepeviktor/phpstan-wordpress extension recognizes common WordPress patterns like hooks, filters, and dynamic function calls. For custom dynamic code, suppress specific errors in your configuration.

What's the performance impact?

PHPStan scans quickly. A 10,000-line codebase typically analyzes in 2-5 seconds. For CI/CD, the overhead is negligible.

Should I use PHPStan in production?

No. PHPStan is a development tool. Run it in your development environment and CI pipeline, not on live servers.

How do I fix "implicitly mixed" errors?

Add explicit type hints. Before: function process( $data ). After: function process( array $data ): string. Be as specific as possible with @param and @return PHPDoc annotations.

Can PHPStan replace unit tests?

No. PHPStan verifies type correctness and code existence. Tests verify business logic and behavior. Use both—they catch different categories of bugs.

Conclusion

Static type analysis is foundational for WordPress plugin quality. PHPStan catches bugs before they reach users, prevents type errors from causing runtime failures, and makes code reviews more focused on logic rather than type correctness.

Start small: install PHPStan, run it on your codebase, and fix the highest-priority errors first. Use baseline files to incrementally adopt stricter rule levels. Integrate PHPStan into your CI pipeline so every commit is analyzed automatically. As you mature, layer in WP HealthKit for comprehensive audits that combine static analysis with security scanning, performance analysis, and WordPress-specific checks.

The time you invest in static analysis compounds. Every bug PHPStan catches is a bug that won't reach production, won't affect users, and won't require emergency fixes.

For more information, see the PHPStan documentation, the szepeviktor/phpstan-wordpress extension, and the WordPress Plugin Security Handbook.


Automate Your Quality Checks

Manual code review is slow. Automated static analysis is fast. Combined with security analysis, performance monitoring, and WordPress-specific audits, you build quality into every release.

Run a free WP HealthKit audit → — comprehensive analysis across 17 verification layers. No credit card required.

Ready to audit your plugin?

WP HealthKit checks for all the issues in this article and 40+ more across 49 verification layers.

Comments

PHPStan for WordPress: Complete Static Analysis Guide | WP HealthKit