PHPackages                             pinkcrab/hook-loader - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. pinkcrab/hook-loader

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

pinkcrab/hook-loader
====================

An object based hook loader for WordPress.

1.3.0(3mo ago)240.6k↑32.1%[3 issues](https://github.com/Pink-Crab/Loader/issues)[1 PRs](https://github.com/Pink-Crab/Loader/pulls)2MITPHPPHP &gt;=8.0.0CI passing

Since Mar 9Pushed 3mo agoCompare

[ Source](https://github.com/Pink-Crab/Loader)[ Packagist](https://packagist.org/packages/pinkcrab/hook-loader)[ Docs](https://pinkcrab.co.uk)[ RSS](/packages/pinkcrab-hook-loader/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (9)Dependencies (24)Versions (17)Used By (2)

Hook\_Loader
============

[](#hook_loader)

An object-based WordPress hook loader. Register actions, filters, AJAX endpoints and shortcodes against `Hook_Loader`, then call `register_hooks()` to commit them to WordPress. Supports admin-only, front-only, and removal of hooks (including hooks registered against class instances you no longer hold).

[![Latest Stable Version](https://camo.githubusercontent.com/8cfe4c2092cfcd632bdc36be08be7ba8333a8a694cb066521b911e080ec9ba30/68747470733a2f2f706f7365722e707567782e6f72672f70696e6b637261622f686f6f6b2d6c6f616465722f76)](https://packagist.org/packages/pinkcrab/hook-loader)[![Total Downloads](https://camo.githubusercontent.com/dd1a71cd30df294dfe836038b8dc03e1cd72d9b43fc7890cf7c6e3995815bcc5/68747470733a2f2f706f7365722e707567782e6f72672f70696e6b637261622f686f6f6b2d6c6f616465722f646f776e6c6f616473)](https://packagist.org/packages/pinkcrab/hook-loader)[![License](https://camo.githubusercontent.com/ad7e1c428c0bec77f844eee5c91e59dc1f858ec67421bae39c96718a45282191/68747470733a2f2f706f7365722e707567782e6f72672f70696e6b637261622f686f6f6b2d6c6f616465722f6c6963656e7365)](https://packagist.org/packages/pinkcrab/hook-loader)[![PHP Version Require](https://camo.githubusercontent.com/e43588a3e9122aa5456c0e8cf8ca4d355832231f2ab56505f5bc1cc467cdba1a/68747470733a2f2f706f7365722e707567782e6f72672f70696e6b637261622f686f6f6b2d6c6f616465722f726571756972652f706870)](https://packagist.org/packages/pinkcrab/hook-loader)[![GitHub contributors](https://camo.githubusercontent.com/3ea06f253591c1150b6a6b1f10ae06aa79a7d0fbd70f3b5e054d6d036ce3d736/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732f50696e6b2d437261622f4c6f616465723f6c6162656c3d436f6e7472696275746f7273)](https://camo.githubusercontent.com/3ea06f253591c1150b6a6b1f10ae06aa79a7d0fbd70f3b5e054d6d036ce3d736/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732f50696e6b2d437261622f4c6f616465723f6c6162656c3d436f6e7472696275746f7273)[![GitHub issues](https://camo.githubusercontent.com/46d1385eca3e193c052082f5f9364688d1b0f56fce4e85858a42f845c811acd1/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6973737565732d7261772f50696e6b2d437261622f4c6f61646572)](https://camo.githubusercontent.com/46d1385eca3e193c052082f5f9364688d1b0f56fce4e85858a42f845c811acd1/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6973737565732d7261772f50696e6b2d437261622f4c6f61646572)

[![WP 6.6 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Loader/actions/workflows/WP_6_6.yaml/badge.svg)](https://github.com/Pink-Crab/Loader/actions/workflows/WP_6_6.yaml)[![WP 6.7 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Loader/actions/workflows/WP_6_7.yaml/badge.svg)](https://github.com/Pink-Crab/Loader/actions/workflows/WP_6_7.yaml)[![WP 6.8 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Loader/actions/workflows/WP_6_8.yaml/badge.svg)](https://github.com/Pink-Crab/Loader/actions/workflows/WP_6_8.yaml)[![WP 6.9 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Loader/actions/workflows/WP_6_9.yaml/badge.svg)](https://github.com/Pink-Crab/Loader/actions/workflows/WP_6_9.yaml)

[![codecov](https://camo.githubusercontent.com/6c58f2ab18273bf20b3bb99f064f2b6eb1b0761991e5b5c7ae9d1eab96953074/68747470733a2f2f636f6465636f762e696f2f67682f50696e6b2d437261622f4c6f616465722f6272616e63682f6d61737465722f67726170682f62616467652e7376673f746f6b656e3d39344446544156414149)](https://codecov.io/gh/Pink-Crab/Loader)[![Scrutinizer Code Quality](https://camo.githubusercontent.com/bf66de834f80f1248c30dcb6cf7a0bd46cd20f44e16f0e08cd5707d3157962b2/68747470733a2f2f7363727574696e697a65722d63692e636f6d2f672f50696e6b2d437261622f4c6f616465722f6261646765732f7175616c6974792d73636f72652e706e673f623d6d6173746572)](https://scrutinizer-ci.com/g/Pink-Crab/Loader/?branch=master)

For more details please visit the docs site: [https://perique.info/lib/Hook\_Loader.html](https://perique.info/lib/Hook_Loader.html)

Why?
----

[](#why)

WordPress — and especially WooCommerce — is built around hooks. Wiring up actions and filters with `add_action()` / `add_filter()` scattered across classes gets hard to reason about quickly: there's no single place to see what's registered, admin-only vs front-only conditions end up duplicated, and removing hooks added by class instances is a known pain.

`Hook_Loader` gives you a single object to declare everything against. You stage your hooks, then flush them to WordPress in one call (`register_hooks()`), which means your class constructors stay side-effect-free and your test harness can inspect the staged hooks before they reach `$wp_filter`.

Install
-------

[](#install)

```
composer require pinkcrab/hook-loader
```

Then include the Composer autoloader in your project:

```
require_once __DIR__ . '/vendor/autoload.php';
```

All registration methods return the created `Hook` object. Nothing binds to WordPress until you call `$loader->register_hooks()` — until then hooks are just staged in the collection.

Methods (Registration)
----------------------

[](#methods-registration)

### action

[](#action)

**action( string $handle, callable $callback, int $args = 1, int $priority = 10 ): Hook**

> @param string $handle Hook handle to register against.
> @param callable $callback Hook callback.
> @param int $args Number of arguments passed to the callback. Default 1.
> @param int $priority Priority the hook fires at. Default 10.
> @return \\PinkCrab\\Loader\\Hook

Registers an action on both admin and front-end contexts. Equivalent to `add_action($handle, $callback, $priority, $args)` when flushed.

*Example*

```
$loader->action( 'init', 'my_init_callback' );
$loader->action( 'save_post', [ $saver, 'handle' ], 2, 20 );
```

### admin\_action

[](#admin_action)

**admin\_action( string $handle, callable $callback, int $args = 1, int $priority = 10 ): Hook**

> @param string $handle Hook handle to register against.
> @param callable $callback Hook callback.
> @param int $args Number of arguments passed to the callback. Default 1.
> @param int $priority Priority the hook fires at. Default 10.
> @return \\PinkCrab\\Loader\\Hook

Same as `action()` but only registers when the request is inside `wp-admin` (checked at flush time via `is_admin()`).

*Example*

```
$loader->admin_action( 'admin_menu', [ $menu, 'register' ] );
```

### front\_action

[](#front_action)

**front\_action( string $handle, callable $callback, int $args = 1, int $priority = 10 ): Hook**

> @param string $handle Hook handle to register against.
> @param callable $callback Hook callback.
> @param int $args Number of arguments passed to the callback. Default 1.
> @param int $priority Priority the hook fires at. Default 10.
> @return \\PinkCrab\\Loader\\Hook

Same as `action()` but only registers on the front-end (when `is_admin()` is false).

*Example*

```
$loader->front_action( 'wp_enqueue_scripts', [ $assets, 'enqueue' ] );
```

### filter

[](#filter)

**filter( string $handle, callable $callback, int $args = 1, int $priority = 10 ): Hook**

> @param string $handle Hook handle to register against.
> @param callable $callback Filter callback; must return the first argument.
> @param int $args Number of arguments passed to the callback. Default 1.
> @param int $priority Priority the hook fires at. Default 10.
> @return \\PinkCrab\\Loader\\Hook

Registers a filter on both admin and front-end contexts.

*Example*

```
$loader->filter( 'the_content', 'my_content_filter' );
$loader->filter( 'wp_nav_menu_items', [ $menu, 'append' ], 2, 50 );
```

### admin\_filter

[](#admin_filter)

**admin\_filter( string $handle, callable $callback, int $args = 1, int $priority = 10 ): Hook**

> @param string $handle Hook handle to register against.
> @param callable $callback Filter callback; must return the first argument.
> @param int $args Number of arguments passed to the callback. Default 1.
> @param int $priority Priority the hook fires at. Default 10.
> @return \\PinkCrab\\Loader\\Hook

Same as `filter()` but only registers inside `wp-admin`.

*Example*

```
$loader->admin_filter( 'post_row_actions', [ $rows, 'add_action' ], 2 );
```

### front\_filter

[](#front_filter)

**front\_filter( string $handle, callable $callback, int $args = 1, int $priority = 10 ): Hook**

> @param string $handle Hook handle to register against.
> @param callable $callback Filter callback; must return the first argument.
> @param int $args Number of arguments passed to the callback. Default 1.
> @param int $priority Priority the hook fires at. Default 10.
> @return \\PinkCrab\\Loader\\Hook

Same as `filter()` but only registers on the front-end.

*Example*

```
$loader->front_filter( 'body_class', [ $body, 'classes' ], 1, 20 );
```

Methods (Removal)
-----------------

[](#methods-removal)

### remove

[](#remove)

**remove( string $handle, $callback, int $priority = 10 ): Hook**

> @param string $handle Hook handle to remove from.
> @param callable|array{0:string,1:string} $callback Callable, or `[class-name, method-name]` array (both strings).
> @param int $priority Priority the target hook was registered at. Default 10.
> @return \\PinkCrab\\Loader\\Hook

WordPress's native `remove_action()` / `remove_filter()` require the *same* callable you passed to `add_action()`. That breaks for hooks added against class instances — you need the original `$instance` and that's often gone or inaccessible. `Hook_Removal` walks `$wp_filter` and matches on class name + method name instead, so a `[class-name, method-name]` array is enough.

*Example*

```
// Match by class name only — no need to reconstruct an instance.
$loader->remove( 'init', [ Some_Other_Plugin_Action::class, 'boot' ], 10 );

// Works equivalently with an instance, if you have one.
$loader->remove( 'init', [ $instance, 'boot' ], 10 );

// Plain callables also work.
$loader->remove( 'init', 'some_global_function', 10 );
```

### remove\_action

[](#remove_action)

**remove\_action( string $handle, $callback, int $priority = 10 ): Hook**

> @param string $handle Hook handle to remove from.
> @param callable|array{0:string,1:string} $callback Callable, or `[class-name, method-name]` array.
> @param int $priority Priority the target hook was registered at. Default 10.
> @return \\PinkCrab\\Loader\\Hook

Alias for `remove()` that signals intent at the call-site when you're removing an action. Identical runtime behaviour — WordPress stores actions and filters in the same `$wp_filter` registry.

*Example*

```
// Global function added via add_action() elsewhere.
$loader->remove_action( 'save_post', 'someone_elses_saver', 10 );

// Instance-method action registered by a third-party plugin — match by class name.
$loader->remove_action(
    'init',
    [ \Other_Plugin\Bootstrap::class, 'register' ],
    10
);

// Or pass a live instance if you happen to hold one.
$loader->remove_action( 'init', [ $existing_instance, 'register' ], 10 );
```

### remove\_filter

[](#remove_filter)

**remove\_filter( string $handle, $callback, int $priority = 10 ): Hook**

> @param string $handle Hook handle to remove from.
> @param callable|array{0:string,1:string} $callback Callable, or `[class-name, method-name]` array.
> @param int $priority Priority the target hook was registered at. Default 10.
> @return \\PinkCrab\\Loader\\Hook

Alias for `remove()` that signals intent at the call-site when you're removing a filter. Identical runtime behaviour to `remove()` / `remove_action()`.

*Example*

```
// Unregister a filter that another plugin added by class.
$loader->remove_filter(
    'the_content',
    [ \Third_Party\Content_Filter::class, 'wrap' ],
    10
);

// Unregister a WP core filter callback by name.
$loader->remove_filter( 'the_content', 'wpautop', 10 );

// Swap a third-party filter for your own at the same priority:
$loader->remove_filter( 'the_title', [ \Other_Plugin\Titles::class, 'prefix' ], 20 );
$loader->filter(        'the_title', [ $this, 'prefix' ], 1, 20 );
$loader->register_hooks();
```

Methods (Shortcodes &amp; Ajax)
-------------------------------

[](#methods-shortcodes--ajax)

### shortcode

[](#shortcode)

**shortcode( string $handle, callable $callback ): Hook**

> @param string $handle Shortcode tag.
> @param callable $callback Shortcode callback. Receives the attributes array and must return a string.
> @return \\PinkCrab\\Loader\\Hook

Registers a shortcode. Runs `add_shortcode()` when `register_hooks()` fires.

*Example*

```
$loader->shortcode( 'my_shortcode', function ( array $atts ): string {
    return esc_html( $atts['text'] ?? '' );
} );

// Somewhere later:
do_shortcode( "[my_shortcode text='hello']" );
```

### ajax

[](#ajax)

**ajax( string $handle, callable $callback, bool $public\_ajax = true, bool $private\_ajax = true ): Hook**

> @param string $handle Ajax action handle (without the `wp_ajax_` / `wp_ajax_nopriv_` prefix).
> @param callable $callback Ajax handler callback.
> @param bool $public\_ajax Register against `wp_ajax_nopriv_` for anonymous users. Default true.
> @param bool $private\_ajax Register against `wp_ajax_` for authenticated users. Default true.
> @return \\PinkCrab\\Loader\\Hook

WordPress splits AJAX into two actions: `wp_ajax_` (authenticated users) and `wp_ajax_nopriv_` (anonymous users). `Hook_Loader::ajax()` registers either or both from a single call.

*Example*

```
$loader->ajax( 'my_action', 'my_callback', true,  true  );  // logged in AND logged out
$loader->ajax( 'my_action', 'my_callback', true,  false );  // logged out only  ($private_ajax=false)
$loader->ajax( 'my_action', 'my_callback', false, true  );  // logged in only   ($public_ajax=false)
```

Methods (Lifecycle)
-------------------

[](#methods-lifecycle)

### register\_hooks

[](#register_hooks)

**register\_hooks(): void**

> @return void

Flushes every staged hook to WordPress. Call once, after all registrations have been declared. Before this is called nothing is bound to `$wp_filter` / `$wp_actions`.

*Example*

```
$loader = new Hook_Loader();
// ...register hooks...
$loader->register_hooks();
```

Use with a class
----------------

[](#use-with-a-class)

Because hooks are staged (not fired immediately), your class constructors stay clean. Expose a `hooks()` method that accepts the loader and records what the class wants registered; the composition root (your plugin bootstrap) flushes them:

```
class Some_Action {
    public function hooks( Hook_Loader $loader ): void {
        $loader->action( 'init', [ $this, 'boot' ] );
        $loader->front_filter( 'the_content', [ $this, 'wrap_content' ], 1, 20 );
    }

    public function boot(): void {
        // side-effecty init
    }

    public function wrap_content( string $content ): string {
        return '' . $content . '';
    }
}

$loader      = new Hook_Loader();
$some_action = new Some_Action();
$some_action->hooks( $loader );
$loader->register_hooks();
```

Filtering hooks before registration
-----------------------------------

[](#filtering-hooks-before-registration)

The hook collection is passed through a filter before it hits WordPress, letting other code mutate, add, or strip hooks at flush time. Use the `Hook_Collection::REGISTER_HOOKS` constant (or its literal string value `pinkcrab/loader/register_hooks`):

```
add_filter( Hook_Collection::REGISTER_HOOKS, function ( $hooks ) {
    // Inspect, mutate, or replace the staged hooks.
    return $hooks;
} );
```

Tested Against
--------------

[](#tested-against)

- PHP 8.0, 8.1, 8.2, 8.3 &amp; 8.4
- WP 6.6, 6.7, 6.8 &amp; 6.9
- MySQL 8.4

License
-------

[](#license)

### MIT License

[](#mit-license)

Change Log
----------

[](#change-log)

- 1.3.0 - Drop PHP 7.x, require PHP 8.0+. Modernise the tooling chain (PHPStan 2.x at level 9, PHPUnit 8|9, WPCS 3.x). Replace the single GitHub\_CI workflow with the WP 6.6–6.9 matrix (PHP 8.0–8.4, `mysql:8.4`) using `codecov/codecov-action@v4`. Suppress the WP 6.8 `wp_is_block_theme` early-call notice in `tests/wp-config.php`. Remove `object-calisthenics/phpcs-calisthenics-rules` from dev-deps. **BC break:** `Hook_Loader::ajax()` and `Hook_Factory::ajax()` parameters `$public` / `$private` renamed to `$public_ajax` / `$private_ajax` (positional callers unaffected; named-argument callers need to update). Reserved-keyword parameter names removed throughout (`$callable` / `$function` → `$callback`).
- 1.2.0 - Updated testing dependencies and support for php8, added in the ability to filter hooks prior to registration.
- 1.1.2 - Loader::class has now been marked as deprecated
- 1.1.1 - Typo on register\_hooks() (spelt at regster\_hooks)
- 1.1.0 - All internal functionality moved over, still has the same ex
- 1.0.2 - Fixed incorrect docblock on Hook\_Loader\_Collection::pop() and adding missing readme entries for shortcode and ajax.
- 1.0.1 - Added pop() and count() to the hook collection. Not used really from outside, only in tests.
- 1.0.0 - Moved from Plugin Core package. Moved the internal collection to there own Object away from PC Collection.

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance59

Moderate activity, may be stable

Popularity30

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity67

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~233 days

Recently: every ~459 days

Total

9

Last Release

109d ago

PHP version history (2 changes)1.0.0PHP &gt;=7.1.0

1.3.0PHP &gt;=8.0.0

### Community

Maintainers

![](https://www.gravatar.com/avatar/d82b9e8ef7816d3d0b9812ad233f61f6a313f529e0ac85721781b46ad292e1ea?d=identicon)[glynnquelch](/maintainers/glynnquelch)

---

Top Contributors

[![gin0115](https://avatars.githubusercontent.com/u/28779094?v=4)](https://github.com/gin0115 "gin0115 (80 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/pinkcrab-hook-loader/health.svg)

```
[![Health](https://phpackages.com/badges/pinkcrab-hook-loader/health.svg)](https://phpackages.com/packages/pinkcrab-hook-loader)
```

###  Alternatives

[wire-elements/wire-extender

Embed your Livewire components anywhere.

28241.3k2](/packages/wire-elements-wire-extender)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
