johnbillion/wp-compat
A lightweight compatibility layer that lets you use WordPress functions and APIs outside of WordPress. Useful for plugins, libraries, and tools that need WordPress-style helpers without loading a full WordPress install.
johnbillion/wp-compat is a PHPStan extension that compares the WordPress APIs your code calls against the @since annotations in the WordPress stubs, and reports anything introduced after your plugin/theme's declared minimum WordPress version. It is a forward-compatibility gate, not a deprecation linter.
Minimum setup:
composer require --dev johnbillion/wp-compat
It pulls in php-stubs/wordpress-stubs (and PHPStan) as dev dependencies. Add it to your PHPStan config:
# phpstan.neon
includes:
- vendor/johnbillion/wp-compat/extension.neon
parameters:
paths:
- src/
# Verify the exact key name against the README for your installed version;
# it is a single scalar declaring your minimum supported WP version.
wpCompat:
minimumVersion: '6.4'
Then run:
vendor/bin/phpstan analyse --memory-limit=-1
Where to look first: the package README.md for the current config key and supported PHPStan version, then extension.neon (it shows exactly which rule is registered and which parameters it reads), then the rule class itself — it is small and tells you precisely what is and isn't detected.
First use case: a legacy plugin whose readme.txt says Requires at least: 5.9 but which has been developed against WP 6.5 for years. Run the extension against src/ and you get an immediate inventory of every function, class, method, or new parameter you're calling that a 5.9 site cannot resolve — typically a handful of wp_* helpers, block-related functions, and newly added optional arguments. That inventory is your upgrade roadmap or your baseline.
Baseline-first adoption. On an existing codebase, do not try to fix hundreds of violations on day one. Generate a PHPStan baseline and commit it, then fail CI only on new violations:
vendor/bin/phpstan analyse --generate-baseline
Treat the baseline as a burn-down list: when you consciously raise your minimum version, bump the configured version and delete the now-satisfied baseline entries.
Two configs, one gate. Keep day-to-day analysis (phpstan.neon, level 5+, your own custom rules) separate from the compatibility gate (phpstan-wpcompat.neon that includes only this extension). Developers run the former constantly; CI runs both. This prevents compat noise from drowning out real type errors and keeps the compat run fast.
CI integration. Run on every PR and use GitHub's annotation format so violations land inline on the diff:
- run: vendor/bin/phpstan analyse -c phpstan-wpcompat.neon --error-format=github
Cache %tmpDir% between runs; stub parsing is the expensive part.
Version-bump workflow. Raising the minimum WP version is a deliberate, reviewable change: bump the config value, run the analysis, and inspect what disappears. Those disappearing violations are exactly the function_exists() polyfills, version branches, and fallback code you can now delete. This is the highest-value recurring use of the package.
Suppressing intentional polyfills. Code that is deliberately guarded belongs in ignoreErrors, ideally by identifier on PHPStan 2.x so it survives refactors:
parameters:
ignoreErrors:
- identifier: wpCompat.*
path: src/Compat/*.php
Require a comment explaining the guard (function_exists( 'wp_foo' )) so reviewers know it's intentional rather than an oversight.
Editor feedback. Point the VS Code PHPStan extension (or Intelephense's PHPStan integration) at the compat config so a developer sees "this function requires WordPress 6.5" while typing, not 20 minutes later in CI.
Keep stubs and the plugin in sync. The rule can only be as accurate as the @since data in the installed stubs. Pin php-stubs/wordpress-stubs and update it deliberately alongside your WP test matrix; an unpinned composer update silently changes which APIs are considered "too new."
It only sees what the stubs see. Dynamic calls are invisible: call_user_func( 'wp_new_thing' ), $fn(), $obj->{$method}(), and string callbacks in add_action are not analyzed. Likewise, new array keys in argument arrays and new hook names are not detected — this is a symbol-level check, not a runtime one.
Hooks are not covered. The package flags functions, classes, methods, and newly added parameters. It does not tell you that some_filter was introduced in 6.3. If hook compatibility matters, you need a separate source of truth (core's @since docs or the hook documentation in php-stubs), and it will be manual.
Forward-compat only. It answers "is this API too new for my minimum?", not "is this API deprecated/removed?". Pair it with WordPress coding standards (WordPress.PHP.DeprecatedFunctions from wp-coding-standards/wpcs) to cover the other direction. Do not expect this package to catch removal in a future release.
Context-blind. Functions only loaded in wp-admin, multisite-only functions, and REST-only helpers are flagged regardless of where your code runs. If a specific file is genuinely admin-only, exclude it by path rather than papering over individual errors, and document the assumption.
Baseline fragility. PHPStan baselines are keyed by message and count; refactors and line moves cause churn. Regenerate after large refactors, set reportUnmatchedIgnoredErrors: false if needed, and prefer identifiers over raw message regexes.
Config key drift. The extension has changed its parameter naming across releases. Pin a version range in composer.json and re-read the README on upgrade — a silently ignored config key means the rule checks against the wrong (or a default) version, which is worse than not running it at all.
Memory and speed. Loading full WordPress stubs is memory-hungry. Always pass --memory-limit=-1 in CI, scope paths tightly to your plugin source (exclude vendor/, tests/, node_modules/), and rely on the result cache.
Small-project risk. With ~25 stars this is a niche, single-maintainer package. The upside is that the rule is small and the source is easy to vendor, fork, or patch if it falls behind a PHPStan major release. Budget for that possibility, and don't make it the only thing standing between you and a broken release — keep a smoke test on a genuine minimum-version WordPress install.
How can I help you explore Laravel packages today?