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
- What is PHPStan and Why It Matters
- Static Analysis vs. Other Quality Tools
- Installation and Initial Setup
- Understanding PHPStan Rule Levels
- Configuring PHPStan for WordPress
- Real Bugs PHPStan Catches
- Integration and CI/CD
- Baseline Files and Gradual Adoption
- 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.
Recommended Levels for WordPress
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.
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.