Introducing Captain Hook for WordPress

One of WordPress’s greatest strengths is its massive plugin ecosystem. As a team that builds bespoke solutions for clients, we often find ourselves working with plugins that either get us 90% of the way toward a feature request, or end up providing more features than we need, leaving us wanting to disable some. Or, perhaps, we want the features enabled for some requests but not all – a common example being disabling features during bulk operations.

Most of the time, WordPress Hooks give us everything we need to make these modifications, but sometimes the way a plugin chooses to hook into actions or filters makes it difficult to modify.

Consider, for example, a plugin that hooks into an action or filter using an instance method. In an attempt to avoid polluting the global scope, the plugin’s author might instantiate the object in a function. In this case, the object instance isn’t accessible to other code, and therefore, we can’t unhook it or otherwise work with that object instance.

To address this issue, we created a small library that we’re calling Captain Hook, which allows us to remove otherwise-inaccessible action/filter callbacks, and even hijack private object instances. Here’s an example of how it works:

class Plugin {
  public function __construct( public string $name ) {
    add_action( 'wp_footer', [ $this, 'wp_footer' ], PHP_INT_MAX );
  }

  public function wp_footer() {
    echo '<p>' . esc_html( "Powered by {$this->name}" ) . '</p>';
  }
}

add_action( 'after_setup_theme', function() {
  new Plugin( 'Super Plugin Pro' );
);

In this example, if we wanted to remove the wp_footer action callback, WordPress doesn’t have a built-in way to do that. Captain Hook makes it possible:

\Alley\WP\remove_action_by_force(
  'wp_footer',
  [ Plugin::class, 'wp_footer' ],
  PHP_INT_MAX
);

This works similarly to unhooking a static method, where you pass the class name and method. The remove_action_by_force function will find the callback where the object’s classname and method name match and remove that callback.

Instead of removing the callback, we can reprioritize it, if we want something else to be able to run before it. Here we move it from a very high priority down to 10:

\Alley\WP\reprioritize_action(
  'wp_footer',
  [ Plugin::class, 'wp_footer' ],
  PHP_INT_MAX,
  10
);

Lastly, we can also hijack the object instance and modify that instance:

$plugin = Alley\WP\get_hooked_object(
  'wp_footer',
  [ Plugin::class, 'wp_footer' ],
  PHP_INT_MAX
);
$plugin->name = 'WordPress';

We hope Captain Hook can make your WordPress development a little easier. If you give it a try, reach out to us on Twitter X or LinkedIn and let us know what you think!