Search by

x3p0-dev / x3p0-hooks

justintadlock

A lightweight, attribute-based hook system for WordPress plugins and themes.

Package info

github.com/x3p0-dev/x3p0-hooks

pkg:composer/x3p0-dev/x3p0-hooks

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 4

Open Issues: 0

1.0.0 2026-06-22 03:49 UTC

This package is auto-updated.

Last update: 2026-09-04 18:18:00 UTC


README

Nova, a blue alien, fishing in the countryside at a pond.

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.

License PHP Version

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 HookPriority enum (First, Normal, Last)
  • Extensible: Define your own hook attributes by implementing the Hook interface
  • 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.

Support