Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phpstan Wordpress Laravel Package

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+.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the extension as a dev dependency — it is not something you ship:

composer require --dev szepeviktor/phpstan-wordpress

If phpstan/extension-installer is already in the project (it usually is in a Laravel/CI setup), the extension is auto-registered. Otherwise wire it manually:

# phpstan.neon.dist
includes:
    - vendor/szepeviktor/phpstan-wordpress/extension.neon

parameters:
    level: 5
    paths:
        - plugin.php
        - inc/

Requirements to check before you start: PHPStan 2.0+ and PHP 7.4+ (tested through PHP 8.3). The PHPStan 2.0 floor is the first real blocker if you are pulling this into a monorepo that still pins PHPStan 1.x for a Larastan-based app.

Where to look first. In order of usefulness:

  1. examples/phpstan.neon.dist in the package — a working config you can copy verbatim.
  2. vendor/szepeviktor/phpstan-wordpress/extension.neon — shows exactly which services, constants, and rules are being injected; this is the file to read when you need to disable or override something.
  3. vendor/php-stubs/wordpress-stubs/wordpress-stubs.php — the source of truth for every WordPress symbol PHPStan thinks exists.
  4. vendor/szepeviktor/phpstan-wordpress/src/ — the dynamic return type extensions; useful when a type is inferred "wrong" and you want to know which extension is responsible.

First use case. Analyze the main plugin file plus a single inc/ directory at level 5, fix the obvious issues, then raise the level one notch at a time. Do not start at level 8 on a mature WordPress plugin; the first run will produce hundreds of findings that are mostly mixed-propagation noise from untyped hook callbacks, and you will lose the team's buy-in before you have fixed anything.

vendor/bin/phpstan analyze --memory-limit=1G

The recommended adoption path is: run once, generate a baseline, commit it, then ratchet down. Everything the extension adds (stubs, dynamic return types, constant definitions, the apply_filters() docblock rule) applies automatically once the extension is included — there is no per-feature setup.

Implementation Patterns

Baseline-first adoption on existing code. This is the pattern that actually works on a legacy theme or plugin:

parameters:
    level: 6
    paths: [plugin.php, inc/]
    baseline: phpstan-baseline.neon
vendor/bin/phpstan analyze --generate-baseline

Then regenerate the baseline only when the number of errors decreases. Treat a growing baseline in a PR as a review flag.

Lean on the apply_filters() docblock contract. This is the package's most distinctive feature and the one to build a habit around. The extension reads the docblock before an apply_filters() call and takes the type of the first @param as the definitive return type:

/**
 * Filters the page title when creating an HTML drop-down list of pages.
 *
 * @param string  $title Page title.
 * @param WP_Post $page  Page data object.
 */
$title = apply_filters( 'list_pages', $title, $page );
// $title is now string, not mixed

Day-to-day rule: every apply_filters() you write or touch gets a docblock where the first @param is accurate. This is the cheapest way to remove mixed propagation from your plugin without sprinkling @var annotations or inline asserts. It also doubles as documentation for third-party integrators.

Type-check your hook callbacks. add_action() / add_filter() callbacks are checked against the hook signature where the stubs know it. Keep callbacks as named methods or invokable classes rather than closures when you want precise parameter types — closures with untyped $args degrade to mixed and poison everything downstream.

WooCommerce shops. Add the WooCommerce stubs on top; the README defers to php-stubs/woocommerce-stubs for specifics. Same shape: composer require --dev php-stubs/woocommerce-stubs plus the include, or use the corresponding extension package if one is available.

Structure your code so the analysis is useful. The README's "Make Your Code Testable" guidance effectively describes how to get value out of PHPStan:

  • One class per file, no code outside class Name { ... } in inc/.
  • Consistent class naming (WPCS or PSR-4) so the Composer autoloader resolves everything. If you cannot use Composer's autoloader, wire a custom one — PHPStan cannot infer types for classes it cannot load.
  • Prefer core functions over core constants. Since 2.0.2 the extension ships a rule that suggests replacing WP constants with function calls; constants are treated as dynamic and lose their literal types.
  • Avoid eval, extract, compact, list, and type juggling. These are exactly where static analysis stops being able to help.

Narrow errors explicitly. This package supports is_wp_error() type narrowing and the assert()-style type-specifying extensions. The pattern is:

$response = wp_remote_get( $url );
if ( is_wp_error( $response ) ) {
    return;
}
// $response is now WP_HTTP_Response-ish, not WP_Error|...

CI integration. Run in a matrix over your supported PHP versions, cache %tmpDir% (the result cache lives there), and fail the build on new errors only. The --error-format=github flag works well with GitHub Actions. Because the package itself is tested on Travis with a PHP version matrix, keeping the matrix aligned with your plugin's Requires PHP header is a reasonable default.

Layer with adjacent tooling. This extension is complementary to, not a replacement for, phpstan/phpstan-strict-rules, phpstan/phpstan-deprecation-rules, and slevomat/coding-standard. Run PHPCS/WPCS for style and PHPStan for types; do not try to make one do the other's job.

Gotchas and Tips

The "Dirty Corner": conditional function definitions. WordPress conditionally declares some functions (e.g. is_gd_image) so they can be overridden by plugins, which means the stubs declare a function that may already exist at runtime. The documented workaround is to rename the stub:

sed -i -e 's#function is_gd_image#function __is_gd_image#' \
  vendor/php-stubs/wordpress-stubs/wordpress-stubs.php

This is a vendor patch. It will be silently reverted on every composer update. Either script it in post-install-cmd/post-update-cmd, or use cweagans/composer-patches so the modification is declarative and fails loudly when it no longer applies.

Wrong first @param = confidently wrong type. The filter docblock rule does not "merge" types; it treats the first @param as truth. A copy-pasted docblock saying @param string $title on a filter that actually returns WP_Post will suppress real bugs rather than surface them. If something looks suspiciously well-typed, check the docblock before trusting the analysis.

No docblock means mixed. The absence of a docblock is not an error — it is silent degradation. This is the single largest source of Cannot call method on mixed reports in WordPress codebases.

Stubs version drift versus your target WordPress. The stubs encode one WordPress version's API surface. If you support an older minimum WP, PHPStan will happily suggest functions that do not exist there. If you support a newer one, recently added functions will be reported as undefined. The 2.0.4 release notably widened the wordpress-stubs constraint to allow v7.0 — so pin this package and php-stubs/wordpress-stubs deliberately rather than floating both.

Dynamic constant list changes between releases. SAVEQUERIES, WP_CLI, TEMPLATEPATH, and STYLESHEETPATH have all been added and removed from dynamicConstantNames across 2.x point releases. If you rely on one of these being treated as literal, check the changelog before upgrading and re-verify after. Overriding a parameters array in your own neon requires the ! prefix to replace rather than merge:

parameters:
    dynamicConstantNames!:
        - MY_PLUGIN_VERSION

Early-terminating call changes alter dead-code results. WP_CLI::error and WP_CLI::halt were removed from earlyTerminatingMethodCalls in 2.0.3 because treating them as terminating was wrong. If you had code after a WP_CLI::error() call that PHPStan previously considered unreachable, expect new findings after upgrading. Conversely, add your own terminators (wp_send_json, wp_die, custom redirect helpers) via earlyTerminatingFunctionCalls so unreachable-code analysis is accurate.

Result cache and patched stubs. After editing anything under vendor/, delete the PHPStan tmpDir (or run with --debug) before re-analyzing. Stale cache masking a stub change is a genuinely confusing hour to lose.

Monorepo / mixed-stack conflict. If you are a Laravel shop with a WordPress plugin alongside it, keep separate phpstan.neon files and separate Composer installs per package. Mixing Laravel stubs and WordPress stubs in one analysis run produces duplicate-symbol and contradictory-signature errors. Also watch for PHPStan version conflicts: this package requires 2.0+, and Larastan's supported PHPStan range moves independently.

Do not suppress the extension's own rule blindly. If the "replace constant with function call" rule fires on something like WP_CONTENT_DIR where no equivalent function exists, ignore it by identifier in a narrow ignoreErrors entry rather than disabling the whole rule — you want it firing on WP_DEBUG-style checks.

Maintenance risk is real and stated. The README carries an explicit notice that the maintainer is considering stepping back from the WordPress ecosystem. Practical mitigations: pin an exact version, vendor the .neon file into your repo if it is small, and keep the extension list thin enough that a fork is feasible. MIT license means a fork is always permitted — plan for a fork or a funded replacement rather than assuming indefinite upstream churn.

Extension points if you need to go further. Write your own PHPStan rule or return-type extension for in-house APIs (REST controllers, custom post types), and register it alongside this package's extension.neon. Add project-specific stubs via stubFiles for third-party plugins you cannot install, and use scanFiles for classes that exist but are not autoloadable. Both are standard PHPStan parameters and compose cleanly with this extension.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table