wp-hooks/wordpress-core
Adds WordPress core action and filter hooks as PHP stubs, giving your IDE autocompletion and static analysis tools like PHPStan and Psalm accurate type information for add_action and add_filter calls.
wp-hooks/wordpress-core is not a runtime library — it is a data package. It ships JSON files describing every action and filter registered by WordPress core (name, source file, type, and the parsed docblock). There is no service provider, no autoloaded classes, and nothing to configure. You install it, read the files, and build your own index on top.
composer require --dev wp-hooks/wordpress-core
The payload lives under the package root:
vendor/wp-hooks/wordpress-core/hooks/actions.json
vendor/wp-hooks/wordpress-core/hooks/filters.json
First use case: stop guessing. Load filters.json, key it by name, and you have an instant lookup for "does this hook exist, where is it fired, and what does the docblock say?" — which is enough to power an Artisan command, a validation rule, or a docs page.
use Illuminate\Support\Facades\File;
$filters = collect(File::json(base_path('vendor/wp-hooks/wordpress-core/hooks/filters.json')))
->keyBy('name');
$filters->get('the_content'); // name, file, type, core, docs...
Start by dumping one entry (dd($filters->first())) and treat the shape you see as the contract — the schema is generated, not hand-curated.
Wrap it in a registry, don't sprinkle File::json around. One class, one parse, both hook types:
final class HookRegistry
{
private ?Collection $hooks = null;
public function all(): Collection
{
return $this->hooks ??= collect(['actions', 'filters'])
->flatMap(fn (string $type) => File::json(
base_path("vendor/wp-hooks/wordpress-core/hooks/{$type}.json")
))
->groupBy('name');
}
public function find(string $name): ?array
{
return $this->all()->get($name)?->first();
}
public function search(string $term): Collection
{
return $this->all()->filter(fn (Collection $group, string $name) =>
str_contains($name, $term)
);
}
}
Cache the parsed result, not the file read. For a web-facing hook browser, serialize a trimmed index once (name, type, file, short description) into the cache or into a generated PHP array file that opcache can hold:
Cache::rememberForever('wp-hook-index', fn () => app(HookRegistry::class)
->all()
->map(fn ($g) => [
'name' => $g->first()['name'],
'type' => $g->first()['type'],
'file' => $g->first()['file'],
'about' => $g->first()['docs']['short_description'] ?? '',
])
->values()
->all());
Validate hook names in configuration. If your app ships a config file of hooks to listen to, bridge to WP, or forward elsewhere, back it with a validation rule so a typo fails in CI rather than silently doing nothing:
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! app(HookRegistry::class)->find($value)) {
$fail("Unknown WordPress core hook [{$value}].");
}
}
Group by source file for a "what runs here" view. The file field is the path in WordPress core that fires the hook. ->groupBy('file') gives you a debugging aid: point it at a core file and list everything it emits.
Use it as a generator input. The most valuable pattern is doc scaffolding: iterate hooks in a namespace-ish family (woocommerce_*, rest_*, save_post_*) and emit stub listener classes or Markdown reference pages from the docs.short_description / docs.long_description fields.
Pair it with wp-hooks/generator when core data isn't enough. That package parses PHP source and produces the same schema for your own plugins/themes; merge its output into the same registry and you get one query surface for core + project hooks.
--no-dev will delete it. This is nearly always installed as a dev dependency, so any feature built on it (docs page, validation rule) breaks in a production build. Either move it to require, or compile the index at deploy time and commit/cache the artifact.WpHooks\... namespaces. Resolve paths via base_path('vendor/...') or, better, Composer\InstalledVersions::getInstallPath('wp-hooks/wordpress-core') so it survives vendor-dir changes.keyBy('name') silently drops entries. Use groupBy if you care about every location.{$taxonomy}_add_form_fields or "{$new_status}_{$post->post_type}". Exact-match validation will reject legitimate dynamic hooks — special-case a str_contains($name, '{') branch and match on the static prefix/suffix.short_description, long_description, tags, and changelog come from parsed docblocks; some hooks have empty or missing docs, and callback signatures live as prose in long_description, not structured args. Don't build a schema that assumes they're populated.File::json() throws on malformed JSON (Laravel 10+). If you point the loader at generated plugin data, wrap it and fail loudly rather than returning an empty collection.HookRegistry API.How can I help you explore Laravel packages today?