PHPStan for WordPress: Setup, Level 10, and CI Guide

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 mixed data.
  • 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 statePractical approach
New pluginStart at level 10 and keep the error count at zero
Maintained plugin with manageable debtRaise the level, fix real errors, and use a reviewed baseline for remaining legacy debt
Large legacy platformConfigure symbols correctly, analyze a bounded area, create an initial baseline, and shrink it continuously
Reusable libraryUse 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

ToolPrimary jobDoes not prove
PHPStanStatic type and control-flow correctnessRuntime integrations or user journeys work
WPCS/PHPCSWordPress conventions and selected security, compatibility, and quality rulesTypes and application behavior are correct
Unit/integration testsExpected behavior for executed scenariosEvery untested path is safe
End-to-end testsCritical workflows across system boundariesInternal 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 patternWhat it usually meansBetter response
Cannot call method on WP_Post|nullThe lookup can failNarrow with an instanceof WP_Post check and define the missing-post behavior
Cannot access offset on mixedAn option, request, or API response has no validated shapeValidate the data and document an array shape
Function expects string, mixed givenSanitization alone has not established a static contractValidate and narrow before calling the function
Method not found on plugin objectPHPStan lacks a stub or the code relies on magic behaviorInstall maintained stubs or create a bounded project stub
Callback return type does not match filterA filter branch returns the wrong value or no valueReturn 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.

Mehul Gohil
Mehul Gohil

Mehul Gohil is a Full Stack WordPress developer and an active member of the local WordPress community. For the last 13+ years, he has been developing custom WordPress plugins, custom WordPress themes, third-party API integrations, performance optimization, and custom WordPress websites tailored to the client's business needs and goals.

Articles: 165

Leave a Reply

Your email address will not be published. Required fields are marked *