urlund / wordpress-feature
Declarative WordPress hook registration for PHP feature classes.
Requires
- php: >=7.4
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Declarative WordPress hook registration for PHP feature classes.
Urlund\WordPress\Feature lets you declare $filters and $actions on a class. On construct, hooks are registered automatically. Parent declarations are merged, instances are tracked in a registry, and hooks can be added, removed, paused, or resumed at runtime.
Requirements
- PHP 7.4+
- WordPress (uses
add_filter/add_action/_doing_it_wrong)
Installation
composer require urlund/wordpress-feature
Available on Packagist. Composer PSR-4 autoloads Urlund\WordPress\Feature from src/Feature.php.
You can also load it manually without Composer. Copy src/Feature.php into your project and require it once:
require_once __DIR__ . '/inc/class-feature.php';
The file is guarded with class_exists, so it is safe to require_once from a plugin and a theme, or alongside Composer.
use Urlund\WordPress\Feature;
Quick start
use Urlund\WordPress\Feature; class Auth extends Feature { protected $filters = [ 'wp_authenticate_user', 'woocommerce_login_redirect' => 20, 'foo_filter' => 'bar_callback', 'baz_filter' => ['baz_callback', 20], ]; protected $actions = [ 'init', 'admin_init' => 20, 'wp_loaded' => 'setup', ]; public function wp_authenticate_user( WP_User $user ) { return $user; } public function woocommerce_login_redirect( string $redirect, WP_User $user ): string { return $redirect; } public function bar_callback( $value, $param1, $param2 ) { return $value; } public function baz_callback( $value, $param1 ) { return $value; } public function init(): void { } public function admin_init(): void { } public function setup(): void { } } Feature::bootstrap( Auth::class );
Hook DSL
Each $filters / $actions entry normalizes to hook name, callback, and priority. The same entry shapes apply to both. Named callbacks resolve to a class method first, then to a global function (function_exists). accepted_args is taken from reflection on the resolved callback (ReflectionMethod or ReflectionFunction).
| Entry | Hook | Method | Priority |
|---|---|---|---|
'wp_authenticate_user' |
same | same | 10 |
'admin_init' => 20 |
same | same | 20 |
'foo_filter' => 'bar_callback' |
foo_filter |
bar_callback |
10 |
'wp_loaded' => ['setup', 20] |
wp_loaded |
setup |
20 |
Global WordPress helpers work when named explicitly:
protected $filters = [ 'show_admin_bar' => '__return_false', 'woocommerce_enable_setup_wizard' => ['__return_false', 20], ];
For multiple callbacks on one hook, see Multiple callbacks.
Invalid shapes throw InvalidArgumentException. Missing callbacks (neither a class method nor a global function) also throw.
Parent merging
Redeclaring $filters / $actions on a child replaces the PHP property, so Feature walks the class hierarchy with Reflection and merges by hook name. Children override parents for the same hook.
class BaseAuth extends Feature { protected $filters = [ 'the_content' ]; protected $actions = [ 'init' ]; } class Auth extends BaseAuth { protected $filters = [ 'wp_authenticate_user', 'the_content' => 20, // overrides parent priority ]; protected $actions = [ 'admin_init', 'init' => 20, // overrides parent priority ]; }
Multiple callbacks
Most features need one callback per hook. When one feature must attach several methods to the same hook, use a method→priority map:
protected $filters = [ 'the_content' => [ 'sanitize' => 10, 'append' => 20, ], ]; public function sanitize( string $content ): string { return $content; } public function append( string $content ): string { return $content; }
The same map form works on $actions. Identity inside a feature is (hook, method); priority is metadata only, not part of the key.
Parent merging still replaces the whole hook config when a child redeclares that hook (a map replaces a single callback and vice versa).
Target one callback or all for a hook:
$auth->pause_filter( 'the_content' ); // all callbacks $auth->pause_filter( 'the_content', 'append' ); // only append $auth->resume_filter( 'the_content', 'append' ); $auth->remove_filter( 'the_content', 'sanitize' );
resume_* and remove_* use the same optional second $method argument (including for actions).
Bootstrap
Instantiate one or many features. Optional file paths load classes that are not autoloaded yet (Auth::class is a compile-time string and does not require the class to exist).
Feature::bootstrap( Auth::class ); Feature::bootstrap( [ Auth::class, Checkout::class, ] ); Feature::bootstrap( [ Auth::class => __DIR__ . '/features/class-auth.php', Checkout::class, ] );
Already-registered features are skipped. Missing classes trigger _doing_it_wrong.
Registry
if ( Feature::has( Auth::class ) ) { Feature::get( Auth::class )->remove_filter( 'baz_filter' ); Feature::get( Auth::class )->remove_action( 'init' ); }
Constructing the same feature class twice triggers _doing_it_wrong and does not re-register hooks. The first instance stays canonical in get().
Runtime add / remove / pause
Same config shapes as the DSL value side (null, int, string, [method, priority], or a method→priority map — see Multiple callbacks):
$auth = Feature::get( Auth::class ); $auth->add_filter( 'the_content', 20 ); $auth->add_action( 'init', 'setup' ); $auth->remove_filter( 'the_content' ); $auth->remove_action( 'init' );
remove_* uses the instance registry so WordPress receives the original resolved callback ([$this, $method] or a global function name) and priority. Remove is permanent (metadata is discarded). An optional second $method argument targets one callback when several are registered on the same hook.
Pause temporarily detaches a hook but keeps method and priority so it can be resumed later:
$auth->pause_filter( 'baz_filter' ); $auth->resume_filter( 'baz_filter' ); $auth->pause_action( 'init' ); $auth->resume_action( 'init' );
Other features (or any plugin code) can pause or resume another feature's hooks via the registry:
Feature::get( Auth::class )->pause_filter( 'baz_filter' ); Feature::get( Auth::class )->resume_filter( 'baz_filter' ); Feature::get( Auth::class )->pause_action( 'init' ); Feature::get( Auth::class )->resume_action( 'init' );
License
MIT