
Wp Phpstan
- 2.5k installs
- 1.9k repo stars
- Updated July 27, 2026
- wordpress/agent-skills
wp-phpstan sets up and fixes PHPStan analysis for WordPress plugins and themes with stubs and baselines.
About
The wp-phpstan skill configures and fixes PHPStan static analysis for WordPress codebases targeting WordPress 6.9 plus with Composer-based PHPStan. Procedure starts with phpstan_inspect.mjs to discover config, baseline, and composer scripts, preferring existing composer run phpstan when present. WordPress core stubs via szepeviktor/phpstan-wordpress or php-stubs/wordpress-stubs are effectively required to avoid unknown function noise. Sane phpstan.neon keeps paths on first-party plugin or theme code, excludes vendor and build artifacts, and documents narrow ignoreErrors entries. Fixes prefer WordPress-specific PHPDoc for REST WP_REST_Request types, hook callback params, query result shapes, and Action Scheduler job args. Third-party plugin classes use real dependency confirmation, plugin stubs like woocommerce-stubs, then targeted ignoreErrors prefixes. Baselines are migration tools not trash bins; do not baseline newly introduced errors. Escalation asks for dependency versions before inventing third-party types. Verification reruns PHPStan after any ignoreErrors pattern changes.
- Run phpstan_inspect.mjs first to discover config and composer entrypoints.
- WordPress stubs are required to avoid mass unknown-function errors.
- Prefer PHPDoc fixes over ignoreErrors for REST and hook callbacks.
- Third-party classes use stubs then narrow vendor-prefix ignores.
- Baselines reduce legacy debt; never baseline new errors.
Wp Phpstan by the numbers
- 2,517 all-time installs (skills.sh)
- +163 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #328 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
wp-phpstan capabilities & compatibility
- Capabilities
- phpstan_inspect.mjs deterministic setup discover · wordpress core stub requirement and config guida · rest, hook, and query phpdoc typing patterns · third party plugin stub and ignoreerrors strateg · baseline migration rules and verification steps
- Use cases
- testing · code review
What wp-phpstan says it does
Without it, expect a high volume of errors about unknown WordPress core functions.
Prefer correcting types over ignoring errors.
npx skills add https://github.com/wordpress/agent-skills --skill wp-phpstanAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.5k |
|---|---|
| repo stars | ★ 1.9k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | wordpress/agent-skills ↗ |
How do I run PHPStan on WordPress code without drowning in core function errors?
Configure, run, and fix PHPStan static analysis in WordPress plugins, themes, and sites with stubs and baselines.
Who is it for?
WordPress plugin and theme repos adding or fixing PHPStan with Composer.
Skip if: Skip for non-PHP WordPress work or projects disallowing Composer dev dependencies without approval.
When should I use this skill?
User configures phpstan.neon, fixes PHPStan errors, or handles third-party WP plugin classes.
What you get
Working phpstan.neon, reduced errors via typing, and controlled baseline for legacy code.
- phpstan.neon configuration
- phpstan-baseline.neon
- Fixed static analysis errors
By the numbers
- Targets WordPress 6.9+ on PHP 7.2.24+
- Requires Composer-based PHPStan installation
Files
WP PHPStan
When to use
Use this skill when working on PHPStan in a WordPress codebase, for example:
- setting up or updating
phpstan.neon/phpstan.neon.dist - generating or updating
phpstan-baseline.neon - fixing PHPStan errors via WordPress-friendly PHPDoc (REST requests, hooks, query results)
- handling third-party plugin/theme classes safely (stubs/autoload/targeted ignores)
Inputs required
wp-project-triageoutput (run first if you haven't)- Whether adding/updating Composer dev dependencies is allowed (stubs).
- Whether changing the baseline is allowed for this task.
Procedure
0) Discover PHPStan entrypoints (deterministic)
1. Inspect PHPStan setup (config, baseline, scripts):
node skills/wp-phpstan/scripts/phpstan_inspect.mjs
Prefer the repo’s existing composer script (e.g. composer run phpstan) when present.
1) Ensure WordPress core stubs are loaded
szepeviktor/phpstan-wordpress or php-stubs/wordpress-stubs are effectively required for most WordPress plugin/theme repos. Without it, expect a high volume of errors about unknown WordPress core functions.
- Confirm the package is installed (see
composer.dependenciesin the inspect report). - Ensure the PHPStan config references the stubs (see
references/third-party-classes.md).
2) Ensure a sane phpstan.neon for WordPress projects
- Keep
pathsfocused on first-party code (plugin/theme directories). - Exclude generated and vendored code (
vendor/,node_modules/, build artifacts, tests unless explicitly analyzed). - Keep
ignoreErrorsentries narrow and documented.
See:
references/configuration.md
3) Fix errors with WordPress-specific typing (preferred)
Prefer correcting types over ignoring errors. Common WP patterns that need help:
- REST endpoints: type request parameters using
WP_REST_Request<...> - Hook callbacks: add accurate
@paramtypes for callback args - Database results and iterables: use array shapes or object shapes for query results
- Action Scheduler: type
$argsarray shapes for job callbacks
See:
references/wordpress-annotations.md
4) Handle third-party plugin/theme classes (only when needed)
When integrating with plugins/themes not present in the analysis environment:
- First, confirm the dependency is real (installed/required).
- Prefer plugin-specific stubs already used in the repo (common examples:
php-stubs/woocommerce-stubs,php-stubs/acf-pro-stubs). - If PHPStan still cannot resolve classes, add targeted
ignoreErrorspatterns for the specific vendor prefix.
See:
references/third-party-classes.md
5) Baseline management (use as a migration tool, not a trash bin)
- Generate a baseline once for legacy code, then reduce it over time.
- Do not “baseline” newly introduced errors.
See:
references/configuration.md
Verification
- Run PHPStan using the discovered command (
composer run ...orvendor/bin/phpstan analyse). - Confirm the baseline file (if used) is included and didn’t grow unexpectedly.
- Re-run after changing
ignoreErrorsto ensure patterns are not masking unrelated issues.
Failure modes / debugging
- “Class not found”:
- confirm autoloading/stubs, or add a narrow ignore pattern
- Huge error counts after enabling PHPStan:
- reduce
paths, addexcludePaths, start at a lower level, then ratchet up - Inconsistent types around hooks / REST params:
- add explicit PHPDoc (see references) rather than runtime guards
Escalation
- If a type depends on a third-party plugin API you can’t confirm, ask for the dependency version or source before inventing types.
- If fixing requires adding new Composer dependencies (stubs/extensions), confirm it with the user first.
PHPStan configuration (WordPress)
This reference documents a minimal, WordPress-friendly PHPStan setup and baseline workflow.
Minimal phpstan.neon template
Use the repo’s existing layout. The example below is intentionally conservative and should be adapted to the project’s actual directories.
# Include the baseline only if the file exists.
includes:
- phpstan-baseline.neon
parameters:
level: 5
paths:
- src/
- includes/
excludePaths:
- vendor/
- vendor-prefixed/
- node_modules/
- tests/
ignoreErrors:
# Add targeted exceptions only when necessary.Guidelines:
- Prefer analyzing first-party code only.
- Exclude anything generated or vendored.
- Keep
ignoreErrorspatterns narrow and grouped by dependency.
Baseline workflow
Baselines help you adopt PHPStan in legacy code without accepting new regressions.
# Generate a baseline (explicit filename)
vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon
# Update an existing baseline (defaults)
vendor/bin/phpstan analyse --generate-baselineBest practices:
- Avoid adding new errors to the baseline; fix the new code instead.
- Treat baseline changes like code changes: review in PRs.
- Chip away at the baseline gradually (remove entries as you fix root causes).
Third-party classes and ignore patterns
When PHPStan reports legitimate classes as missing (e.g. because WordPress or a plugin is not installed in the analysis environment), prefer fixing discovery first and only then add targeted ignores.
Before adding ignoreErrors
- Confirm the dependency is real (installed/required in this environment).
- Prefer stubs/extensions already used by the repo.
- Prefer a narrow ignore for the vendor prefix over a broad ignore.
Recommended stub packages
Stubs are useful when the analysis environment does not include WordPress (or a plugin API) but you still want real type checking (instead of blanket ignores).
Common packages:
composer require --dev szepeviktor/phpstan-wordpress
composer require --dev php-stubs/wordpress-stubs
composer require --dev php-stubs/woocommerce-stubs
composer require --dev php-stubs/acf-pro-stubsWhen stubs are useful (and sometimes necessary):
- Running PHPStan in a plugin/theme repo without a full WordPress checkout.
- PHPStan reports unknown WordPress core functions (e.g.
add_action(),get_option()). - Integrations with optional plugins (WooCommerce, ACF Pro) that are not installed during analysis.
- You want method/property existence checks and accurate return types instead of
ignoreErrors.
Notes:
- Prefer stubs that match the runtime versions; mismatches can cause false positives.
- Adding Composer dependencies changes the repo; confirm it is acceptable for the task.
Ensure stubs are loaded
Installing stubs is not enough if PHPStan does not scan them. Add stub paths in phpstan.neon.
parameters:
bootstrapFiles:
- %rootDir%/../../php-stubs/woocommerce-stubs/woocommerce-stubs.php
scanFiles:
- %rootDir%/../../php-stubs/wordpress-stubs/wordpress-stubs.php
- %rootDir%/../../php-stubs/acf-pro-stubs/acf-pro-stubs.php
- %rootDir%/../../woocommerce/action-scheduler/functions.phpTargeted ignore patterns (examples)
parameters:
ignoreErrors:
# Admin Columns Pro
- '#.*(unknown class|invalid type|call to method .* on an unknown class) AC\\ListScreen.*#'
# Elementor
- '#.*(unknown class|invalid type|call to method .* on an unknown class) Elementor\\.*#'
# Yoast SEO
- '#.*(unknown class|invalid type|call to method .* on an unknown class) WPSEO_.*#'Pattern creation rules:
- Cover error variations:
unknown class,invalid type,call to method .* on an unknown class. - Keep patterns specific enough to target only intended classes.
- Add a short comment naming the plugin/theme.
- Group related patterns for the same dependency.
When to add exceptions:
- Only for legitimate third-party dependencies your code integrates with.
- Document each pattern with a comment.
- Re-run PHPStan to ensure the ignore does not hide unrelated issues.
WordPress-specific type annotations
These patterns help PHPStan understand WordPress code where runtime behavior and dynamic typing make inference difficult.
REST API request typing
PHPStan cannot infer valid request parameters from REST API schemas. Provide explicit type hints for request params.
/**
* Handle REST API request.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, error on failure.
*
* @phpstan-param WP_REST_Request<array{
* post?: int,
* orderby?: string,
* meta_key?: string,
* per_page?: int,
* status?: array<string>
* }> $request
*/
public function get_items( $request ) {
$post_id = $request->get_param( 'post' );
// PHPStan now knows $post_id is int|null.
}For complex schemas, define reusable types.
/**
* @phpstan-type PostRequestParams array{
* title?: string,
* content?: string,
* status?: 'publish'|'draft'|'private',
* meta?: array<string, mixed>
* }
*
* @phpstan-param WP_REST_Request<PostRequestParams> $request
*/Hook callbacks
/**
* Handle status transitions.
*
* @param string $new_status
* @param string $old_status
* @param WP_Post $post
*/
function handle_transition( string $new_status, string $old_status, WP_Post $post ): void {
// ...
}
add_action( 'transition_post_status', 'handle_transition', 10, 3 );Database and iterables
/**
* @return array<WP_Post> WP_Post objects.
*/
function get_custom_posts(): array {
$posts = get_posts( [ 'post_type' => 'custom_type', 'numberposts' => -1 ] );
return $posts;
}
/**
* @return array<object{id: int, name: string}> Database results.
*/
function get_user_data(): array {
global $wpdb;
$results = $wpdb->get_results( "SELECT id, name FROM users", OBJECT );
return $results ?: [];
}Hooks (apply_filters() and do_action())
Docblocks for apply_filters() and do_action() are validated. The type of the first @param is definitive.
If a third party returns the wrong type for a filter, a PHPStan error is expected and does not require defensive code.
/**
* Allows hooking into formatting of the price.
*
* @param string $formatted The formatted price.
* @param float $price The raw price.
* @param string $locale Locale to localize pricing display.
* @param string $currency Currency symbol.
*/
return apply_filters( 'autoscout_vehicle_price_formatted', $formatted, $price, $locale, $currency );Action Scheduler argument shapes
/**
* Process a scheduled email.
*
* @param array{user_id: int, email: string, data: array<string, mixed>} $args
*/
function process_scheduled_email( array $args ): void {
$user_id = $args['user_id'];
// ...
}
as_schedule_single_action(
time() + 3600,
'process_scheduled_email',
[
'user_id' => 123,
'email' => 'user@example.com',
'data' => [ 'key' => 'value' ],
]
);import fs from "node:fs";
import path from "node:path";
const TOOL_VERSION = "0.1.0";
/**
* Reads and parses JSON from a file path.
*
* Returns null when parsing fails so the caller can provide user-facing
* guidance without crashing.
*
* @param {string} filePath Absolute path to a JSON file.
* @returns {any|null} Parsed JSON object.
*/
function readJsonSafe(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch {
return null;
}
}
/**
* Reads a UTF-8 text file.
*
* Returns null when reading fails so callers can surface missing configs
* without crashing.
*
* @param {string} filePath Absolute path to a text file.
* @returns {string|null} File contents.
*/
function readTextSafe(filePath) {
try {
return fs.readFileSync(filePath, "utf8");
} catch {
return null;
}
}
/**
* Checks whether a path exists and is a regular file.
*
* @param {string} filePath Absolute or relative file path.
* @returns {boolean} True when the path exists and is a file.
*/
function isFile(filePath) {
try {
return fs.statSync(filePath).isFile();
} catch {
return false;
}
}
/**
* Normalizes Composer script entries into a flat list of commands.
*
* Composer allows scripts to be strings or arrays. This helper provides a
* consistent format for analysis.
*
* @param {unknown} value Composer script value.
* @returns {string[]} Command list.
*/
function normalizeComposerScript(value) {
if (typeof value === "string") return [value];
if (Array.isArray(value)) return value.filter((x) => typeof x === "string");
return [];
}
/**
* Detects which Composer scripts invoke PHPStan.
*
* This helps the agent prefer the repo's own invocation (memory limits,
* config, bootstrap files) instead of guessing.
*
* @param {Record<string, unknown>} scripts Composer scripts block.
* @returns {Array<{name: string, commands: string[]}>} Matching script entries.
*/
function findPhpstanScripts(scripts) {
if (!scripts || typeof scripts !== "object") return [];
const matches = [];
for (const [name, raw] of Object.entries(scripts)) {
const commands = normalizeComposerScript(raw);
const invokesPhpstan = commands.some((cmd) => {
if (typeof cmd !== "string") return false;
return cmd.includes("phpstan") || cmd.includes("vendor/bin/phpstan");
});
if (!invokesPhpstan) continue;
matches.push({ name, commands });
}
return matches;
}
/**
* Chooses a recommended command for running PHPStan in the current repo.
*
* The intent is to prefer an existing Composer script (often has correct
* config, bootstrap, and memory limits), falling back to vendor binaries.
*
* @param {Array<{name: string, commands: string[]}>} phpstanScripts Matching Composer scripts.
* @param {{binaryRelPath: string|null, configRelPath: string|null}} fallbackInfo Fallback discovery.
* @returns {{command: string|null, rationale: string}} Suggested command and why.
*/
function suggestCommand(phpstanScripts, fallbackInfo) {
const preferred = phpstanScripts.find((s) => s.name === "phpstan");
if (preferred) {
return {
command: `composer run ${preferred.name}`,
rationale: "Uses the repo's Composer script (preferred for consistent config).",
};
}
if (phpstanScripts.length > 0) {
return {
command: `composer run ${phpstanScripts[0].name}`,
rationale: "Uses the repo's Composer script that invokes PHPStan.",
};
}
if (!fallbackInfo.binaryRelPath) {
return {
command: null,
rationale: "No PHPStan binary detected under vendor/bin and no Composer script found.",
};
}
const configArg = fallbackInfo.configRelPath ? ` -c ${fallbackInfo.configRelPath}` : "";
return {
command: `${fallbackInfo.binaryRelPath} analyse${configArg}`,
rationale: "Falls back to vendor/bin/phpstan with an explicit config when needed.",
};
}
/**
* Extracts lightweight hints from a phpstan.neon config.
*
* This does not parse NEON. It only checks for common directive tokens so the
* agent can quickly see whether scan directives are in use.
*
* @param {string} configText Raw phpstan config contents.
* @returns {{mentionsScanDirectories: boolean, mentionsScanFiles: boolean}} Hints.
*/
function buildConfigHints(configText) {
const t = configText.toLowerCase();
return {
mentionsScanDirectories: t.includes("scandirectories"),
mentionsScanFiles: t.includes("scanfiles"),
};
}
/**
* Extracts stub-like package references from a PHPStan config.
*
* The PHPStan config usually references stubs via vendor paths (for example,
* "vendor/php-stubs/wordpress-stubs"), so this helper focuses on composer-style
* "vendor/package" tokens containing "stubs".
*
* @param {string} configText Raw phpstan config contents.
* @returns {string[]} Unique, lowercased composer-style package references.
*/
function extractStubPackageReferences(configText) {
const matches = configText
.toLowerCase()
.match(/\b[a-z0-9_.-]+\/[a-z0-9_.-]*stubs[a-z0-9_.-]*\b/g);
if (!matches) return [];
return [...new Set(matches)].sort();
}
/**
* Builds a JSON report describing the current repository's PHPStan setup.
*
* @returns {object} A stable, machine-readable inspection report.
*/
function buildReport() {
const repoRoot = process.cwd();
const composerPath = path.join(repoRoot, "composer.json");
const composer = isFile(composerPath) ? readJsonSafe(composerPath) : null;
const phpstanConfigFiles = ["phpstan.neon", "phpstan.neon.dist"].filter((f) =>
isFile(path.join(repoRoot, f))
);
const phpstanBaselineFiles = ["phpstan-baseline.neon", "phpstan-baseline.neon.dist"].filter((f) =>
isFile(path.join(repoRoot, f))
);
let configRelPath = null;
if (phpstanConfigFiles.includes("phpstan.neon")) configRelPath = "phpstan.neon";
else if (phpstanConfigFiles.includes("phpstan.neon.dist")) configRelPath = "phpstan.neon.dist";
const configAbsPath = configRelPath ? path.join(repoRoot, configRelPath) : null;
const configText = configAbsPath ? readTextSafe(configAbsPath) : null;
const binaryRelPath = isFile(path.join(repoRoot, "vendor", "bin", "phpstan")) ? "vendor/bin/phpstan" : null;
const composerScripts = composer?.scripts && typeof composer.scripts === "object" ? composer.scripts : null;
const phpstanScripts = composerScripts ? findPhpstanScripts(composerScripts) : [];
const composerDependencies = [
...Object.keys(composer?.require ?? {}),
...Object.keys(composer?.["require-dev"] ?? {}),
].sort();
const referencedDependencies = configText ? extractStubPackageReferences(configText) : [];
const configHints = configText ? buildConfigHints(configText) : null;
const suggested = suggestCommand(phpstanScripts, {
binaryRelPath,
configRelPath: configRelPath === "phpstan.neon" ? null : configRelPath,
});
const notes = [];
if (!composer) notes.push("No composer.json found; PHPStan is usually installed via Composer.");
if (phpstanConfigFiles.length === 0) notes.push("No phpstan.neon or phpstan.neon.dist found at repo root.");
if (!binaryRelPath && phpstanScripts.length === 0) notes.push("No PHPStan entrypoint detected (Composer script or vendor/bin/phpstan).");
return {
tool: { name: "phpstan_inspect", version: TOOL_VERSION },
repoRoot,
composer: {
exists: Boolean(composer),
path: isFile(composerPath) ? "composer.json" : null,
phpstanScripts,
dependencies: composerDependencies,
},
phpstan: {
configFiles: phpstanConfigFiles,
baselineFiles: phpstanBaselineFiles,
config: {
primary: configRelPath,
hints: configHints,
referencedDependencies,
},
binary: {
vendorBin: binaryRelPath,
},
},
suggested,
notes,
};
}
/**
* CLI entrypoint for printing the inspection report.
*/
function main() {
const report = buildReport();
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
}
main();
Related skills
How it compares
Use wp-phpstan for WordPress-specific PHPStan setup and fixes; generic PHPStan guides lack WordPress hook, REST, and third-party plugin class patterns.
FAQ
Are WordPress stubs optional?
Effectively required; without stubs expect high volume of unknown WordPress core function errors.
How should third-party plugin classes be handled?
Confirm the dependency, try plugin stubs, then add narrow ignoreErrors for the vendor prefix.
When is a baseline appropriate?
Once for legacy code migration, then shrink over time; never for newly introduced errors.
Is Wp Phpstan safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.