A lightweight, attribute-based hook system for WordPress plugins and themes. Built with PHP 8.1+, it lets you register WordPress actions and filters declaratively with #[Action] and #[Filter] attributes instead of manual add_action()/add_filter() plumbing.
- Attribute-Based Hooks: Register WordPress actions and filters declaratively with
#[Action]and#[Filter] - Hook Any Member: Attach hooks to methods, properties, or class constants
- Repeatable Attributes: Register a single member on multiple hooks
- Priority Presets: Use integers or the type-safe
HookPriorityenum (First,Normal,Last) - Extensible: Define your own hook attributes by implementing the
Hookinterface - Lightweight: Minimal overhead, no external dependencies
- Type-Safe: Full PHP 8.1+ type declarations for better IDE support
- PHP 8.1 or higher
- WordPress (recommended latest version)
- Composer
Install via Composer:
composer require x3p0-dev/x3p0-hooksImportant: If you're releasing this as part of a theme or plugin bundle, please vendor prefix your installation to avoid conflicts with other plugins/themes.
Add the Hookable trait to a class, mark members with #[Action] or
#[Filter], then call registerHookCallbacks() from your own method. The trait
reflects the class and wires every attributed member to WordPress:
use X3P0\Hooks\Action;
use X3P0\Hooks\Filter;
use X3P0\Hooks\Hookable;
final class Assets
{
use Hookable;
// Your own lifecycle method β name it whatever fits your project.
public function boot(): void
{
$this->registerHookCallbacks();
}
#[Action('wp_enqueue_scripts')]
public function enqueue(): void
{
wp_enqueue_style('theme', get_stylesheet_uri());
}
#[Filter('body_class')]
public function bodyClass(array $classes): array
{
$classes[] = 'x3p0-theme';
return $classes;
}
}
// Wire up the hooks.
(new Assets())->boot();The number of arguments passed to the callback is taken from the method's
parameter count, so bodyClass() above receives the $classes argument
automatically.
Hookable provides the protected registerHookCallbacks() method that does the
work. When called, it reflects the class and registers every method, property,
and class constant marked with a hook attribute. Methods of any visibility are
supported β protected and private methods are bound and registered as closures,
so they work as hook callbacks without being public.
The trait exposes no public method on purpose: it stays out of your component
lifecycle and lets the consuming class decide when registration runs and under
what name. Call registerHookCallbacks() from your own method β a boot(),
init(), or whatever contract your project already uses.
registerHookCallbacks() throws a ReflectionException if the class cannot be
reflected.
#[Action] registers a member via add_action(), and #[Filter] registers it
via add_filter(). Both accept the hook name and an optional priority:
#[Action('init')]
public function setup(): void
{
// ...
}
#[Filter('the_content', priority: 20)]
public function content(string $content): string
{
return $content;
}The attributes are repeatable, so a single member can be attached to several
hooks. Priority accepts an integer or a HookPriority case β First
(PHP_INT_MIN), Normal (10), or Last (PHP_INT_MAX):
use X3P0\Hooks\HookPriority;
#[Action('init')]
#[Action('wp_loaded', priority: HookPriority::First)]
public function bootstrap(): void
{
// Runs on both `init` (priority 10) and `wp_loaded` (priority PHP_INT_MIN).
}Properties and class constants can be hooked too, in which case their value is returned to the filter. This is a concise way to provide a static filter value:
#[Filter('big_image_size_threshold', priority: HookPriority::Last)]
protected const THRESHOLD_WIDTH = 3480;
#[Filter('excerpt_length')]
private int $excerptLength = 40;Attributes are matched by the Hook interface using
ReflectionAttribute::IS_INSTANCEOF, so you can define your own hook attributes
by implementing Hook (or extending Filter β which is exactly how Action is
built), and the trait will pick them up automatically.
The Hook interface defines a single method:
namespace X3P0\Hooks;
interface Hook
{
public function register(callable $callback, int $arguments = 1): void;
}This package fires no WordPress hooks of its own β you decide when
registerHookCallbacks() runs. Registering on an appropriate hook (such as
init or after your services are constructed) keeps registration predictable:
add_action('after_setup_theme', function (): void {
(new Assets())->boot();
});Group related hooks by responsibility (assets, admin, REST, etc.) rather than
collecting unrelated hooks in a single class. This keeps each class small and
its registerHookCallbacks() cost minimal.
If you're distributing your plugin/theme, consider using a tool like PHP-Scoper to avoid conflicts.
X3P0 Hooks is licensed under the GPL-2.0-or-later license.
Created and maintained by Justin Tadlock under the X3P0 umbrella.
