x3p0-dev / x3p0-hooks
A lightweight, attribute-based hook system for WordPress plugins and themes.
Requires
- php: >=8.1
Requires (Dev)
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-04 18:18:00 UTC
README
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.
Features
- 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
Requirements
- PHP 8.1 or higher
- WordPress (recommended latest version)
- Composer
Installation
Install via Composer:
composer require x3p0-dev/x3p0-hooks
Important: 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.
Quick Start
Mark members with #[Action] or #[Filter], then hand your object to a
HookRegistrar. It reflects the class and wires every attributed member to
WordPress:
use X3P0\Hooks\Action; use X3P0\Hooks\Filter; use X3P0\Hooks\HookRegistrar; final class Assets { public function __construct( private readonly HookRegistrar $registrar ) {} // Your own lifecycle method — name it whatever fits your project. public function boot(): void { $this->registrar->registerFor($this); } #[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; } } // Construct one HookRegistrar for your whole plugin or theme (e.g., via // your DI container) and reuse it everywhere. $registrar = new HookRegistrar(); // Wire up the hooks. (new Assets($registrar))->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.
Core Concepts
The Hookable Interface and HookRegistrar
Hookable defines a single-method contract for registering an object's
attribute-defined hooks:
namespace X3P0\Hooks; interface Hookable { public function registerFor(object $target): void; }
HookRegistrar is the library's implementation, built on reflection. When
registerFor($target) is called, it reflects the target's 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 library places no requirement on your component lifecycle: call
registerFor() from wherever your project already boots things — a boot(),
init(), or whatever contract you use.
Reflecting a class and reading its attributes is comparatively expensive, but
the result is identical for every instance of the same class, so
HookRegistrar caches registrations per class name for the life of the
object. That cache only pays off if the same HookRegistrar instance handles
every hookable class in the request — construct one (e.g., via your DI
container) and reuse it everywhere, rather than creating one per object.
registerFor() throws a ReflectionException if the class cannot be
reflected.
The #[Action] and #[Filter] Attributes
#[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). }
Hooking Properties and Constants
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;
Custom Hook Attributes
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 HookRegistrar 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; }
Best Practices
Decide When to Register
This package fires no WordPress hooks of its own — you decide when
registerFor() runs. Registering on an appropriate hook (such as init or
after your services are constructed) keeps registration predictable:
add_action('after_setup_theme', function () use ($registrar): void { (new Assets($registrar))->boot(); });
Keep Hook-Registering Classes Focused
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 registerFor() cost minimal.
Vendor Prefix When Necessary
If you're distributing your plugin/theme, consider using a tool like PHP-Scoper to avoid conflicts.
License
X3P0 Hooks is licensed under the GPL-2.0-or-later license.
Credits
Created and maintained by Justin Tadlock under the X3P0 umbrella.
