szepeviktor/phpstan-wordpress
PHPStan extension that enables static analysis of WordPress plugins and themes: loads WordPress stubs, adds dynamic return types, defines core constants, and validates apply_filters/do_action docblocks. Requires PHPStan 2.0+ and PHP 7.4+.
What the package actually is. szepeviktor/phpstan-wordpress is a PHPStan extension (config fragment + PHP code) that teaches PHPStan about WordPress. It does three jobs:
php-stubs/wordpress-stubs so WordPress core functions/classes resolve during static analysis.get_sites(), term_exists(), wp_parse_url(), has_filter(), is_wp_error(), the *_slash() family, current_time(), etc.).@param docblocks preceding apply_filters() / do_action(), treating the first @param as the authoritative return type of the filter.Architecture fit. This is a dev-time only dependency. It ships no runtime code into the WordPress plugin/theme, so it carries zero production attack surface, zero runtime latency, and zero hosting constraint. That is a very favourable risk profile for a TPM: the blast radius of failure is limited to CI and developer laptops. Architecturally it slots into the standard composer.json require-dev + extension.neon include model, which means it composes cleanly with existing PHPStan config, baselines, and level settings rather than competing with them. It is an additive layer on tooling the team already runs or should already be running.
Integration feasibility. Friction is low: one Composer command, and if phpstan/extension-installer is present, literally nothing else. Manual install is a two-line NEON include. Requirements are PHPStan ≥ 2.0 and PHP ≥ 7.4 — both are baseline-modern for any actively maintained WordPress codebase in 2025+. No service account, no secrets, no network egress, no data leaving the build agent. The real work is not installing it; it is adopting it, i.e., triaging the baseline of pre-existing errors it will surface on a legacy plugin.
Technical risk.
sed command that monkey-patches vendor/php-stubs/wordpress-stubs/wordpress-stubs.php to rename conditionally-defined functions. Any recommended workflow that requires mutating a vendored file inside vendor/ is fragile: it breaks on composer update, it is invisible to code review, and it is not reproducible without a wrapper script checked into the repo. This is a genuine engineering-debt marker.instanceof usage, version-conditional assert types, moved extensions). A PHPStan major bump is a coordinated upgrade, not a drop-in.php-stubs/wordpress-stubs tracking WordPress core. The v2.0.4 release exists purely to widen the stubs version range for 7.0 — evidence that version-range pinning is a recurring maintenance chore.apply_filters() feature trusts the first @param. If the docblock is wrong, PHPStan will confidently propagate the wrong type and can mask real bugs. This inverts the usual assumption that the analyser is conservative.Key questions for the maintainer / team.
sed stubs workaround be replaced by an upstream stubs change or a supported config flag?Stack fit. Ideal for any team running WordPress plugins/themes with Composer-managed dev dependencies and a CI pipeline (GitHub Actions, GitLab CI, Bitbucket). It is a quality gate for PHP, sitting alongside PHPCS/WPCS (style) and PHPUnit (behaviour). Position it explicitly as the type-correctness layer, not a replacement for WordPress Coding Standards — WPCS enforces formatting and i18n; this enforces type flow and API misuse.
Recommended topology.
szepeviktor/phpstan-wordpress and phpstan/phpstan to require-dev at the plugin repository root, not the WordPress install root.phpstan/extension-installer to avoid hand-maintained NEON includes per repo.phpstan.neon.dist with level, paths, and includes; keep machine-local overrides in an untracked phpstan.neon.php-stubs/woocommerce-stubs per the README's pointer — these compose.^2.0) and lock via composer.lock; treat lockfile bumps as reviewed changes.Migration path (avoid the "wall of red" failure mode).
level 0, record error count and wall-clock time. This is a one-day investigation.vendor/bin/phpstan analyse --generate-baseline. Commit the baseline. This is the crucial political step — it lets the team enforce zero new errors immediately without a freeze on feature work.apply_filters() conventions: because the first @param is treated as definitive, add a review checklist item for hook docblocks. Bad docblocks become actively harmful here.Compatibility notes.
sed stubs patch unless strictly required; if required, wrap it in a documented Composer script so it is reproducible and visible.WP_CLI::launch's exit_error per the README's testing guidance; it interferes with analysability.Maintenance. The steady-state cost is low but non-zero: PHPStan and stubs version bumps arrive several times a year, and each is a small, reviewable dependency PR. Budget roughly one engineer-day per quarter per org, plus the PHPStan major-version migration every ~18–24 months, which is a multi-day coordinated effort across repos. The dominant maintenance item is not the tool; it is baseline hygiene — without a policy that the baseline only shrinks, it silently becomes a permanent suppression file and the investment decays to zero.
Support. Community-only, best-effort, no SLA, no vendor. There is a single primary maintainer who has publicly signalled intent to exit. For an organisation where static analysis is a gating CI check, that is an unacceptable sole-source position unless mitigated. Mitigations, in order of preference: (a) fork to an internal namespace or a team-owned org and vendor it via a private Composer registry; (b) mirror the package internally and pin, accepting delayed updates; (c) accept the risk explicitly because the failure mode is "CI goes red once and you remove one line" rather than a production outage. Option (c) is defensible precisely because this is a dev dependency — that is the key judgement call for a TPM here.
Scaling. Analysis cost grows superlinearly with codebase size and with PHPStan level. Mitigations: enable PHPStan's parallel processing (parallel.maximumNumberOfProcesses), set memory_limit explicitly (PHPStan is memory-hungry at higher levels), split analysis across multiple NEON config files or per-plugin CI jobs rather than one monolith, and cache Composer between runs. At 50+ repos, switch to a reusable workflow with a shared config package so a level bump is one change, not fifty. CI minutes are the real budget line — full-repo analysis at high levels on a large plugin can run into minutes; diff-scoped gating keeps this cheap.
Failure modes to plan for.
| Failure | Symptom | Mitigation |
|---|---|---|
| Maintainer abandons | No PHPStan 3 support; stubs drift | Internal fork / mirror; monitor releases |
| PHPStan major bump | Extension incompat, mass false positives | Pin PHPStan; upgrade as a tracked project |
| Stubs drift vs WP core | Unknown function/class errors | Keep stubs range wide; bump with WP releases |
Bad apply_filters() docblock |
Wrong type propagated, real bug masked | Review checklist; treat hook docblocks as code |
sed stubs workaround lost |
CI passes locally, fails clean | Composer script, not tribal knowledge |
| Baseline never shrinks | Metric looks green, code still has debt | Enforce monotonic baseline reduction in review |
Ramp-up. Developer learning curve is the most underestimated cost. Two distinct skills: (1) PHPStan itself — levels, @phpstan- annotations, generics, baselines, ignoring errors correctly rather than with ignoreErrors carpet-bombing; (2) WordPress-specific typing — WP_Error union handling, filter return types, dynamic constants. Expect 1–2 days per engineer to productive, and designate one "static analysis champion" per team who owns config and triages false positives. Ship a short internal playbook covering: how to fix vs. how to baseline vs. when to file upstream, and the house rule that @param on a filter docblock is a public API contract.
TPM summary. This is a high-leverage, low-cost, low-blast-radius developer-productivity investment with an unusually clean risk profile (dev-only, MIT, no runtime footprint) undermined by a clearly disclosed single-maintainer sustainability problem and one fragile vendor-patching workaround. Recommended posture: adopt, but own your exit — internal fork or mirror as a contingency, baseline-plus-ratchet as the rollout pattern, and explicit acceptance that the worst case is a stalled CI gate rather than a broken product.
How can I help you explore Laravel packages today?