php-stubs/wordpress-stubs
Stub declarations for WordPress core functions, classes, and interfaces (globals excluded), generated from johnpbloch's wordpress-core via php-stubs/generator. Helps plugin and theme developers leverage static analysis tools like PHPStan and Psalm, and improves IDE code completion and intellisense.
Install the stubs as a dev dependency:
composer require --dev php-stubs/wordpress-stubs:^7.1
Dev-only — nothing ships to production. v7.1.2 stubs are generated from WordPress 7.1.2 core, so keep the constraint aligned with the WP version you actually target.
Configure PHPStan (the normal path for Laravel projects):
composer require --dev szepeviktor/phpstan-wordpress
# phpstan.neon
includes:
- vendor/szepeviktor/phpstan-wordpress/extension.neon
The extension already pulls the stubs in, so requiring php-stubs/wordpress-stubs directly is only needed for IDE support or if you pin a specific stub version. If you do require both, make sure the constraint matches (^7.1).
First use case: analyse a single plugin class.
./vendor/bin/phpstan analyse app/Plugins/MyPlugin/Classes/MyClass.php --memory-limit=1G
Errors like "Function wp_insert_post not found" and "Class WP_Post not found" disappear; what remains is real type feedback on your own code.
Low-risk patch release, but it changes inferred types, so expect new (and fewer old) PHPStan errors on a clean run:
composer update php-stubs/wordpress-stubs --with-dependencies
./vendor/bin/phpstan clear-cache
./vendor/bin/phpstan analyse --memory-limit=1G
Diff the output against your previous baseline — anything new is either a genuine bug or a stub-level tightening (see Gotchas). If you commit a PHPStan baseline, regenerate it after reviewing the diff, not before.
{ "intelephense.stubs": ["wordpress", "Core", "standard"] }
If you need the pinned version instead, add the vendor path to intelephense.environment.includePaths.Settings > PHP → add vendor/php-stubs/wordpress-stubs/wordpress-stubs.php as an Include Path (external library), not as a project source root. Keeping it outside the project tree stops it being indexed as first-class code./app
/Plugins
/MyPlugin
/Classes
MyCustomPostType.php <-- analysed
my-plugin.php <-- WordPress bootstrap
parameters:
level: 8
paths:
- app/Plugins
excludePaths:
- vendor
- node_modules
- tests
includes:
- vendor/szepeviktor/phpstan-wordpress/extension.neon
Workflow: write WP logic in plain classes → analyse → fix → commit. Run it on a pre-commit hook or CI so stub-version drift is caught on the PR that triggers it, not weeks later.
// app/Services/WPPostService.php
namespace App\Services;
use WP_Post;
class WPPostService
{
/** @param array{post_title: string, post_content: string, post_status?: string} $data */
public function createPost(array $data): WP_Post
{
$postId = wp_insert_post($data, true); // throws WP_Error instead of returning 0
return get_post($postId);
}
}
Because wp_insert_post() is typed int|WP_Error, passing true as the second argument narrows the return. Prefer that over $postId === 0 checks — PHPStan now tracks it for you.
v7.1.2 fixed several conditional signatures. Use them instead of casting:
// paginate_links() returns array when 'type' => 'array' (string otherwise, void on empty)
/** @return list<string> */
public function paginationLinks(): array
{
return paginate_links(['type' => 'array', 'total' => 5]) ?? [];
}
// absint() is now int<0, max>
$count = absint($_POST['count'] ?? 0);
Pass an array literal for type and PHPStan resolves the branch — no @var cast required. Drop any manual (array)/(string) casts around these calls; they mask the very bug the stubs now catch.
<psalm>
<projectFiles>
<directory name="app/Plugins" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<stubs>
<file name="vendor/php-stubs/wordpress-stubs/wordpress-stubs.php" />
</stubs>
</psalm>
Keep WordPress core out of <projectFiles> — only the stub file goes in <stubs>.
name: PHPStan
on: [push, pull_request]
jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --prefer-dist --no-progress
- run: ./vendor/bin/phpstan analyse --memory-limit=1G --no-progress
Pin the stub version in composer.lock and let Dependabot bump it — a stub bump should land as its own PR so any new errors are attributable.
Stub version ≠ your WordPress version.
v7.1.2 stubs mirror core 7.1.2. If your plugin still supports 6.x, functions and parameters introduced later will type-check against your code even though they don't exist at runtime on old sites. Either pin an older stub tag for legacy-support branches, or add ignoreErrors entries for the specific modern functions you guard with function_exists().
v7.1.2 tightens some signatures — this is the main upgrade risk.
wp_is_stream() now asserts a non-falsy string for $path. realpath($p) is string|false, so wp_is_stream(realpath($p)) now errors — guard the false case explicitly.add_settings_field() param types were completed; loosely-typed argument arrays will surface as errors.absint() and paginate_links() conditional returns were corrected. Code that assumed a plain int / string may now report int<0, max> or array mismatches.
Treat all of these as either a real bug or a deliberate narrowing — never blanket-suppress before reading the message.Sealed array shapes are looser, not stricter. The "sealed array excluding valid cases" fix removes false positives: passing extra keys to $args arrays no longer trips a sealed-shape error. If you previously added @phpstan-ignore-line for those, remove them or they'll be flagged as unmatched ignores.
$wpdb and other globals are not typed. Classes are stubbed; global variables aren't. Annotate at the point of use:
/** @var \wpdb $wpdb */
global $wpdb;
Better: wrap DB access in a repository/service so the global appears once.
Private methods aren't stubbed. Only public API is described. Code calling WP_Query internals will report undefined methods — that's correct feedback; replace with public APIs.
Stale caches hide the upgrade. PHPStan caches result and stub data. After any stub bump, run ./vendor/bin/phpstan clear-cache before concluding "nothing changed".
./vendor/bin/phpstan analyse --debug prints the resolved config and loaded stub files. Check the extension.neon include path and that the vendor package actually installed.grep -n "function wp_is_stream" vendor/php-stubs/wordpress-stubs/wordpress-stubs.php. It's a single generated PHP file; it's the ground truth for what PHPStan sees.parameters:
stubFiles:
- app/Stubs/my-plugin.stub
(Use stubFiles, not includes — includes expects a NEON config, not a PHP file.)WP_Post": the file is missing from <stubs>, or a copy of WP core got into <projectFiles> and is shadowing it.Custom rules — enforce project policy over WP calls:
namespace App\PHPStan;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
/** @implements Rule<FuncCall> */
class NoDirectWpDbRule implements Rule
{
public function getNodeType(): string { return FuncCall::class; }
public function processNode(Node $node, Scope $scope): array
{
$name = $node->name->toString();
if ($name === 'wpdb_insert' || str_contains($name, 'global_wpdb')) {
return ['Use the repository layer instead of direct $wpdb access.'];
}
return [];
}
}
Register in phpstan.neon under services:. Useful for keeping WP globals out of Laravel controllers.
Stub patching between releases — if a signature is wrong and blocking you, patch the stub locally via stubFiles overriding the function, and open an issue upstream. Don't edit vendor/ (it disappears on composer install).
Regenerating for a specific core version — fork/clone php-stubs/generator, require johnpbloch/wordpress:<version>, run ./generate.sh, and diff against upstream to see exactly what changed.
--memory-limit=1G for medium projects, 2G for full-plugin analysis. Clear it only when stubs or config change.tests/, vendor/, node_modules/, built assets from paths.phpstan analyse --parallel (or --memory-limit accordingly) — stub-heavy analysis parallelises well.Help > Change Memory Settings; Intelephense: intelephense.files.maxSize) rather than excluding the stubs — excluding them reintroduces the undefined-symbol warnings you installed the package for.How can I help you explore Laravel packages today?