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

Wp Hooks Laravel Package

morningtrain/wp-hooks

A Laravel package bringing WordPress-style actions and filters to your PHP applications. Register, fire, and remove hooks with a familiar API, making it easy to add extensibility and event-driven customization to your code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up autoloading

composer require morningtrain/wp-hooks

The package assumes Composer autoloading is present in your WordPress runtime. If you're on Bedrock or a Sage/Roots-style theme, this is already handled. On a classic install, add a require_once __DIR__ . '/vendor/autoload.php'; near the top of functions.php (or from a must-use plugin) before anything registers hooks.

Where to look first

  1. README.md — the package's primary API surface is small, so the usage examples there are effectively the whole contract.
  2. src/ — locate the main hook-registration class/facade. That one file tells you whether the API is static (Hook::action(...)), instance-based, or class-attribute driven.
  3. composer.json — check the PHP version constraint and whether it declares a roots/wordpress or johnpbloch/wordpress-core dependency, since that dictates whether you can drop it into an existing non-Bedrock site.

First use case: taming functions.php

Pick one self-contained feature — a custom post type, a set of admin notices, a content filter — and move only that into a dedicated class. Register its hooks through the package instead of scattering add_action / add_filter calls. You get a single file that owns the entire feature, and deleting the file plus one registration line removes the feature cleanly.

// functions.php or a plugin bootstrap
Feature\BookPostType::register();

The point of the exercise isn't the syntax; it's proving to yourself that a feature can be described in one place. Once that works for one feature, the migration of the remaining hooks is mechanical.

Implementation Patterns

One class per feature, not one class per hook

The failure mode of "organize your hooks" packages is creating a Hooks.php that becomes a second functions.php. The pattern that actually pays off is a class per domain concern — PostTypes\Book, Admin\DashboardWidgets, Frontend\ContentFilters — each exposing a static register() that declares its own hooks. The bootstrap file becomes a short manifest:

Book::register();
DashboardWidgets::register();
ContentFilters::register();

Separate admin and frontend registration

Wrap registration in context checks so you don't load admin-only callbacks on every frontend request:

if (is_admin()) {
    Admin\Settings::register();
} else {
    Frontend\ContentFilters::register();
}

Be deliberate about which hook you check context on. is_admin() is available early; is_user_logged_in() and most conditionals are not reliable before init or wp_loaded.

Chain priority and argument count at the declaration site

The main ergonomic win over raw WordPress is co-locating the callback with its priority and accepted-args. Keep the declaration and the callback adjacent so a reader never has to hunt for add_filter('the_content', ..., 99, 2) elsewhere. If the package supports fluent configuration, use it; if it only takes arrays, prefer named constants for priorities used in more than one place (Priority::LATE = 99) so ordering intent survives refactors.

Filters must return; actions must not

Establish a convention in your codebase: every filter callback in a feature class returns the value, every action callback returns void. Mixed contracts are the single most common source of "the whole page went blank" bugs when adopting any hook-organizing layer, because the indirection makes the missing return harder to spot in review.

Register late enough, but not too late

If a feature class depends on theme supports, plugins, or options, register its hooks on after_setup_theme or plugins_loaded rather than at file load. Conversely, anything that must run on plugins_loaded itself has to be registered before that — a classic off-by-one. Pick an earliest-safe boundary per feature and document it in the class docblock.

Test the callbacks, not the hook layer

The hook registration itself is thin; the value is in the callbacks. Structure feature classes so the callback is a public (or protected + tested via subclass) method that takes plain arguments. Then unit tests call $instance->filterContent('<p>x</p>') directly without booting WordPress. Reserve integration tests for verifying the hook is actually attached (has_filter).

Keep the package at the edges

Treat it as sugar, not architecture. Feature classes should be usable from a plain add_action if the dependency ever disappears. That means no package-specific base class baked into your domain logic where avoidable — a thin register() static that happens to call into the package is easy to swap later.

Gotchas and Tips

Autoload timing beats everything

If vendor/autoload.php isn't loaded before your hook files execute, you get a fatal on the class name rather than a friendly "hooks not firing" symptom. Confirm autoloading early in functions.php or, better, from an mu-plugin that loads first. On Bedrock this is handled; on hand-rolled installs it is the number one setup failure.

Closures can't be unhooked

If the API encourages closure callbacks, remember remove_action / remove_filter needs the identical callable. A closure registered by the package is effectively permanent. For anything a third party (or a future you) might need to disable — theme compatibility shims, third-party plugin overrides — register a named static method or a first-class callable instead.

Double registration is silent

WordPress does not complain when the same callback is added twice at the same priority; it just runs twice. When refactoring hooks into classes, a feature that used to be registered in functions.php and is now also registered via the new class will run its side effects twice — duplicated meta boxes, doubled content, repeated emails. Grep the old codebase for the hook names before deleting, and delete in the same commit.

Priority defaults are not your friend for filters that must win

The default priority of 10 means "arrives in registration order, which depends on plugin load order." If your filter must run after a specific plugin's, use an explicit late priority. If it must run before, use a low one. Never rely on load order — Composer's autoload ordering and plugin activation order both change it.

Debugging: verify attachment before debugging the callback

global $wp_filter;
var_dump(isset($wp_filter['the_content']));

or has_filter('the_content', [ContentFilters::class, 'filter']). Query Monitor's Hooks panel is the fastest way to see what's attached, at what priority, and from which file. If the callback never fires, it's almost always a registration-timing or context-guard problem, not a logic bug.

Filter callbacks receiving the wrong number of args

accepted_args defaults to 1. A callback declared with two parameters but registered without accepted_args = 2 gets null for the second — no warning, no error, just silently missing data. When a filter callback starts producing bizarre results, check the arg count first.

doing_it_wrong on early hook use

Calling functions like wp_enqueue_script, add_shortcode, or translation functions before their hooks fire produces _doing_it_wrong notices — and with WP_DEBUG on, those notices can break JSON responses and redirects. Guard feature registration behind the correct lifecycle hook rather than at autoload time.

Bus factor and version pinning

This is a two-star package with no dependents, maintained by a single agency. That's fine — it's small and MIT — but pin the version in composer.json, commit composer.lock, and make sure the abstraction is shallow enough that removal is a one-hour job. If the repo goes quiet, you want the option to inline the handful of lines it provides rather than fork it.

Extension points

Read the source for the base registration class before you need it: subclassing it, overriding a resolver method, or supplying your own callable-normalization is usually the sanctioned way to handle unusual cases (e.g. registering a hook whose callback needs dependency injection). If no such seam exists, resist patching vendor/ — wrap the package in your own one-file helper instead.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager