PHPStan is a static analysis tool that finds type errors and invalid assumptions in PHP code without executing it. In WordPress projects, it can detect incorrect hook contracts, nullable values used unsafely, invalid method calls, incomplete branches, and data that remains an unknown mixed type.
PHPStan is most valuable when it runs on every pull request, understands WordPress through maintained stubs and extensions, and applies strict rules to new code. It complements WordPress Coding Standards and tests; it does not replace either.
What Does PHPStan Do in a WordPress Project?
- Checks that functions and methods receive compatible arguments.
- Verifies return values and property assignments.
- Finds undefined classes, functions, methods, properties, and variables.
- Detects unsafe operations on nullable values and
mixeddata. - Identifies unreachable branches and impossible type checks.
- Uses PHPDoc, stubs, and extensions to understand behavior that native PHP types cannot express.
PHPStan does not crawl a running WordPress site or test browser behavior. It analyzes source code and the symbol information available to it.
Why WordPress Needs PHPStan Stubs and Extensions
A plugin calls WordPress functions and classes that are not defined inside the plugin repository. Loading all of WordPress during analysis would add side effects and still would not fully describe dynamic behavior. Stubs declare the signatures PHPStan needs without executing WordPress.
The WordPress stubs project provides declarations for core functions, classes, and interfaces. The phpstan-wordpress extension loads those stubs and adds dynamic return-type knowledge, WordPress constants, and hook-related analysis.
How to Install PHPStan for WordPress
Install PHPStan, the WordPress extension, and the PHPStan extension installer as development dependencies:
composer require --dev
phpstan/phpstan
phpstan/extension-installer
szepeviktor/phpstan-wordpress
The extension installer registers compatible PHPStan extensions automatically. If your project does not use it, add the extension manually:
includes:
- vendor/szepeviktor/phpstan-wordpress/extension.neon
A Strict PHPStan Configuration for a WordPress Plugin
Create phpstan.neon.dist in the project root:
parameters:
level: 10
paths:
- plugin.php
- src
excludePaths:
analyse:
- vendor
- build
tmpDir: .cache/phpstan
reportUnmatchedIgnoredErrors: true
Replace the paths with your own main plugin file and source directories. Do not analyze third-party code in vendor; PHPStan’s configuration reference recommends analyzing code you own while discovering dependency symbols separately.
Run the analysis:
vendor/bin/phpstan analyse --configuration=phpstan.neon.dist --no-progress
Which PHPStan Level Should WordPress Use?
PHPStan currently provides 11 cumulative levels from 0 to 10. The official rule-level documentation defines level 10 as the strictest and notes that it reports implicit mixed, including missing types.
| Project state | Practical approach |
|---|---|
| New plugin | Start at level 10 and keep the error count at zero |
| Maintained plugin with manageable debt | Raise the level, fix real errors, and use a reviewed baseline for remaining legacy debt |
| Large legacy platform | Configure symbols correctly, analyze a bounded area, create an initial baseline, and shrink it continuously |
| Reusable library | Use strict native types and precise PHPDoc; consider additional strict rules |
level: max is an alias for the highest available level and can adopt stricter checks when PHPStan adds them. Pinning level: 10 makes the current contract explicit. Choose intentionally based on your upgrade policy.
Why Level 10 Exposes WordPress Problems
WordPress APIs often return unions such as a value or false, an object or null, or trusted and untrusted arrays with unspecified shapes. Level 10 forces code to narrow those possibilities before using them.
<?php
declare( strict_types=1 );
$post = get_post( $post_id );
if ( ! $post instanceof WP_Post ) {
return;
}
$title = get_the_title( $post );
if ( '' === $title ) {
return;
}
echo esc_html( $title );
The checks are not noise. They define what the code does when the requested post does not exist or has no title. That explicit behavior is what makes strict analysis useful in production systems.
Use Array Shapes for WordPress Data
Native PHP cannot fully describe the structure of many associative arrays. PHPStan’s PHPDoc types can document keys, optional fields, list contents, non-empty strings, and object-like shapes.
/**
* @phpstan-type Settings array{
* enabled: bool,
* endpoint: non-empty-string,
* timeout?: positive-int
* }
*
* @return Settings
*/
function acme_get_settings(): array {
return array(
'enabled' => true,
'endpoint' => 'https://api.example.com',
'timeout' => 10,
);
}
Shapes are particularly useful for REST payloads, plugin options, block attributes, hook arguments, and integration responses. Validate runtime data before asserting a narrow static type.
Type WordPress Hooks Carefully
Hooks are dynamic contracts. The callback’s accepted argument count and types must match what the hook supplies. Filter callbacks must return the expected value type on every path.
/**
* Add a marker to a post title.
*/
function acme_filter_title( string $title, int $post_id ): string {
if ( $post_id <= 0 || '' === $title ) {
return $title;
}
return $title . ' — Verified';
}
add_filter( 'the_title', 'acme_filter_title', 10, 2 );
The WordPress extension can interpret documented apply_filters() contracts, but custom hooks still need accurate documentation and stable behavior.
Use Baselines Without Hiding New Debt
A baseline records the current set of errors so PHPStan can report new violations. The official baseline guide recommends it for a manageable existing error set, not as a substitute for correct configuration or a way to ignore thousands of unknown problems.
vendor/bin/phpstan analyse
--configuration=phpstan.neon.dist
--generate-baseline=phpstan-baseline.neon
Review the generated file, include it from the project configuration, and make baseline count reduction part of maintenance. Do not regenerate it automatically in CI because that accepts new errors.
Run PHPStan in Continuous Integration
name: Static Analysis
on:
pull_request:
push:
branches:
- main
jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: none
- run: composer install --no-interaction --prefer-dist
- run: vendor/bin/phpstan analyse --configuration=phpstan.neon.dist --no-progress
Test supported PHP versions separately when behavior varies by runtime. Keep Composer’s lock file committed for applications and plugins that build a release artifact, and update dependencies deliberately.
PHPStan vs WPCS vs Tests
| Tool | Primary job | Does not prove |
|---|---|---|
| PHPStan | Static type and control-flow correctness | Runtime integrations or user journeys work |
| WPCS/PHPCS | WordPress conventions and selected security, compatibility, and quality rules | Types and application behavior are correct |
| Unit/integration tests | Expected behavior for executed scenarios | Every untested path is safe |
| End-to-end tests | Critical workflows across system boundaries | Internal code quality or all edge cases |
Use PHPStan alongside WordPress Coding Standards and an automated plugin build and deployment workflow. For teams, enforce the same checks through shared development and CI rules.
If a plugin or WordPress platform has accumulated type debt, fragile integrations, or inconsistent engineering controls, my WordPress plugin development service covers modernization as well as new development.
Common PHPStan Errors in WordPress Code
| Error pattern | What it usually means | Better response |
|---|---|---|
| Cannot call method on WP_Post|null | The lookup can fail | Narrow with an instanceof WP_Post check and define the missing-post behavior |
| Cannot access offset on mixed | An option, request, or API response has no validated shape | Validate the data and document an array shape |
| Function expects string, mixed given | Sanitization alone has not established a static contract | Validate and narrow before calling the function |
| Method not found on plugin object | PHPStan lacks a stub or the code relies on magic behavior | Install maintained stubs or create a bounded project stub |
| Callback return type does not match filter | A filter branch returns the wrong value or no value | Return the original compatible value on every non-changing path |
Do not silence an error until you can explain the runtime contract. An ignore rule is appropriate for a proven analyzer limitation, not for code that happens to work with today’s data.
Handle Third-Party Plugins and Missing Types
WooCommerce, ACF, Gravity Forms, WP-CLI, and other ecosystems expose symbols outside WordPress core. Prefer stubs maintained by the upstream project or a focused stubs package. Confirm that the package version matches the dependency range your plugin supports.
When no reliable stubs exist, create a small project stub containing only the APIs your code uses. Do not copy an entire plugin into the analysis paths. A stub describes symbols; it should not execute application code or claim stronger types than the dependency guarantees.
Keep stub assumptions under review during dependency upgrades. A clean analysis based on an inaccurate stub can be more dangerous than a visible error because it creates false confidence.
Frequently Asked Questions
Does PHPStan work with WordPress?
Yes. Use maintained WordPress stubs and a WordPress-specific PHPStan extension so the analyzer understands core functions, classes, constants, dynamic return types, and documented hook behavior.
What PHPStan level should a new WordPress plugin use?
Start new code at level 10 when the supported dependencies provide sufficient type information. It is cheaper to maintain strictness from the first commit than to retrofit missing types later.
Should a legacy WordPress project start at level 10?
The target can be level 10, but the migration should be controlled. Configure symbols correctly, choose a bounded scope, fix high-risk errors, create a reviewed baseline for remaining debt, and prevent new errors.
Does PHPStan replace WordPress Coding Standards?
No. PHPStan analyzes types and control flow. WPCS enforces WordPress conventions and selected security, compatibility, and quality rules. Run both.
Does a clean PHPStan run prove a plugin has no bugs?
No. It proves that the analyzed code passed the configured static rules with the available type information. Runtime behavior, integrations, permissions, security, performance, accessibility, and user workflows still need appropriate tests and review.
Should phpstan.neon be committed to Git?
Commit the shared configuration, commonly as phpstan.neon.dist. Developers can keep an ignored local phpstan.neon for environment-specific overrides, but CI should use the reviewed project configuration.





