PHPStan static analysis for WordPress
PHPStan catches type errors, dead code, and bad calls without running anything. It doesn't load WordPress, so it needs stubs for core's functions/classes — szepeviktor/phpstan-wordpress provides those plus WP-aware rules. This skill wires it up and tames the WP-specific noise. PHPCS (wp-phpcs-coding-standards) covers style; PHPStan covers types/logic — run both.
When to use this skill
- Adding static analysis to a plugin/theme.
- Writing or reviewing
phpstan.neon/phpstan.neon.dist. - Raising the analysis level on existing code (and baselining the backlog).
- Killing WP-specific false positives (
apply_filtersarg counts,is_wp_error(),$wpdb, conditional constants).
Install
composer require --dev szepeviktor/phpstan-wordpress
composer require --dev phpstan/extension-installer # recommended: auto-registers the extension
szepeviktor/phpstan-wordpress (2.0.3) pulls in phpstan/phpstan ^2.0 and php-stubs/wordpress-stubs ^6.6.2 transitively — you don't normally add those yourself. It requires PHPStan 2.0+.
At the WordPress 7.1 audit date, the newest tagged wordpress-stubs release is
7.0.1. Composer can therefore analyze ordinary plugin code, but new 7.1-only
symbols may still be reported as unknown. Do not silence those errors broadly.
Feature-detect the API for older WordPress support and add a small reviewed
project stub for the missing 7.1 declaration, then remove it when an official
7.1 stubs package is available.
phpstan/extension-installer is a Composer plugin that auto-includes any installed phpstan-extension package's config, so you don't hand-wire the include. Composer 2.2+ needs it in allow-plugins:
{
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
}
}
}
Configure (phpstan.neon.dist)
With extension-installer, the config is minimal — the extension auto-registers:
parameters:
level: 5
paths:
- my-plugin.php
- includes/
Without extension-installer, include the extension manually (exact path, verified):
includes:
- vendor/szepeviktor/phpstan-wordpress/extension.neon
parameters:
level: max
paths:
- my-plugin.php
- includes/
ignoreErrors:
# WP filter functions use func_get_args(); calling with extra args is fine.
- '#^Function apply_filters(_ref_array)? invoked with [34567] parameters, 2 required\.$#'
You normally do not set bootstrapFiles for WP core — the extension already loads php-stubs/wordpress-stubs. Add bootstrapFiles only for extra stubs (see WooCommerce below) or your own define-shims.
Levels
PHPStan has levels 0–10 (0 loosest, 10 strictest; level 10 was added in PHPStan 2.0). max is an alias for the highest. Start where the code passes (often 5), commit that, then raise one level at a time. Don't jump to max on a legacy plugin — baseline instead (below).
Stubs, and cross-plugin analysis
PHPStan can't see WordPress, so wordpress-stubs supplies typed, implementation-free declarations of core functions/classes. The package is versioned to the WordPress version it was generated from.
To analyze code that calls another plugin's API (e.g. WooCommerce), add that plugin's stubs and register them:
composer require --dev php-stubs/woocommerce-stubs
parameters:
bootstrapFiles:
- vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php
php-stubs/woocommerce-stubs depends on wordpress-stubs, so WP stubs come along. Without the right stubs, PHPStan reports "unknown function/class" for every external call.
The baseline workflow (legacy code)
Don't fight thousands of pre-existing errors. Snapshot them and only gate new/changed code:
vendor/bin/phpstan analyse --generate-baseline
This writes phpstan-baseline.neon (every current error). Include it:
includes:
- phpstan-baseline.neon
Now CI is green, but any new error fails the build. Over time, fix entries and regenerate a smaller baseline, or raise the level behind the baseline. This is how you adopt PHPStan (or a higher level) on an existing plugin without a giant rewrite.
WP-specific false positives the extension handles
phpstan-wordpress fixes much of the usual WordPress noise:
apply_filters()"invoked with N parameters, 2 required" — WP filters usefunc_get_args(). The extension's official example supplies the narrowignoreErrorsregex shown above; the installedextension.neonitself does not contain that ignore.- Loose return types — dynamic return-type extensions narrow
apply_filters(),esc_sql(),wp_parse_url(),shortcode_atts(),wp_slash(), and more. is_wp_error()narrowing — afterif ( is_wp_error( $x ) ), PHPStan knows$xis aWP_Error(vs the success type) in each branch.- Hook callbacks —
HookCallbackRulevalidatesadd_action()/add_filter()callback signatures;HookDocsRulevalidates the docblocks onapply_filters()/do_action()calls. - WordPress constants — the extension's
dynamicConstantNameslist (WP_DEBUG,WP_DEBUG_LOG,WP_DEBUG_DISPLAY,SCRIPT_DEBUG,ABSPATH,WP_CONTENT_DIR,WP_PLUGIN_DIR, …) plus itsWpConstantFetchRuletell PHPStan those WordPress constants may be conditionally (re)defined, so it won't wrongly narrowif ( WP_DEBUG )-style checks. - Early-terminating functions —
wp_send_json*andwp_nonce_aysare known to end execution, so PHPStan won't flag "undefined variable after" them.
One it can't infer for you: global $wpdb;. Annotate it where used:
global $wpdb;
/** @var \wpdb $wpdb */
Run it
vendor/bin/phpstan analyse # uses phpstan.neon(.dist)
vendor/bin/phpstan analyse -l 8 includes/ # override level + path
vendor/bin/phpstan analyse --memory-limit 1G # large plugins + stubs eat RAM
Add a composer script so it joins the QA entry point alongside phpcs/phpunit:
"scripts": {
"analyze": "phpstan analyse"
}
Critical rules
- Install
szepeviktor/phpstan-wordpress(it brings PHPStan 2.0 + WP stubs) — don't hand-assemble stubs. - Use
phpstan/extension-installer(andallow-plugins) so the extension auto-registers; otherwise add theextension.neoninclude manually. - Pick a level the code passes, commit, raise gradually.
maxon legacy code without a baseline is noise, not signal. - Baseline legacy errors with
--generate-baselineso only new/changed code is gated. - Add the right stubs for cross-plugin calls (e.g.
php-stubs/woocommerce-stubs) or every external symbol is "unknown". - Use only the extension's documented narrow suppression for the extra-argument
apply_filters()pattern; do not generalize it to unrelated calls. The extension handles return narrowing, hooks, constants, andis_wp_error()separately. Annotateglobal $wpdbwith/** @var \wpdb $wpdb */. - Check stub freshness against the target core. For WordPress 7.1, tagged 7.0.1 stubs do not describe every new symbol; a green run alone is not a complete 7.1 API audit.
Cross-references
- Run
wp-phpcs-coding-standardsfor the complementary style/standards gate (PHPCS = style, PHPStan = types/logic). - Run
wp-phpunit-test-setupto add PHPStan to the same composer scripts and CI matrix. - Run
wp-plugin-architecturewhen PHPStan findings point at structural issues (untyped boundaries, god objects). - Run
wp-security-audit— PHPStan catches type bugs, not security ones; pair them.
References
- phpstan-wordpress: https://github.com/szepeviktor/phpstan-wordpress
- Rule levels: https://phpstan.org/user-guide/rule-levels
- The baseline: https://phpstan.org/user-guide/baseline
- WordPress stubs: https://github.com/php-stubs/wordpress-stubs
- WooCommerce stubs: https://github.com/php-stubs/woocommerce-stubs
- Extension installer: https://github.com/phpstan/extension-installer
- Related documentation: https://github.com/szepeviktor/phpstan-wordpress/blob/master/extension.neon
- Related documentation: https://github.com/szepeviktor/phpstan-wordpress/blob/master/examples/phpstan.neon.dist