Our 19th door in our Advent Calendar!

Hooks have been a pillar of WordPress development pretty much since WordPress exists. Even if actions and filters have quite a different scope, their internal implementation is almost identical. In this blogpost I want to give an introduction into how to deal with WordPress hooks with a focus on how to remove WordPress Hooks that use objects.

What makes a Hook

Every action or filter is made by 4 elements:

  • a “hook name”, sometimes referred as “hook tag” or just “action” / “filter” or even “hook”
  • a callback
  • a priority
  • an optional set of arguments

Internally WordPress generates also a callback identifier and its value is predictable only for named functions and static class methods. For the other three kinds of callbacks the value is based on spl_object_hash() and so it is not predictable.

The non-predictability of the identifier causes a well-known issue with hooks that uses objects: they are hard to remove.

A deeper Look at the Issue

When either remove_action or remove_filter are called, WordPress calculates the identifier of the callback to remove based on the arguments passed to those functions, then removes from the registered hooks for the specific tag and priority, the one that uses the callback identified with the calculated identifier.

For this reason, to be sure to succeed in removing, the best thing to do is to pass to remove_action / remove_filter the exact arguments (tag, callback and priority) that were used to add the hook.

When callbacks involves objects “exact” means “exact instance”.

For example:
class SomeClass {
  
  public function __construct() {
    
    add_action( 'init', [ $this, 'on_init' ] );
    
    add_action( 'shutdown', [ $this, 'on_shutdown' ] );
  }
}

The actions added in constructor can’t be removed from outside SomeClass like this:


remove_action( 'init', [ $this, 'on_init' ] );

because $this is only accessible inside SomeClass.

In this example:
function add_some_hooks() {
  
  add_action( 'init', function() { /* something happen here */ } );
  
  add_action( 'shutdown', [ new SomeClass(), 'on_shutdown' ] );
}

the action added with the closure and the action added with a just-created SomeClass instance, can’t be removed, because there’s no way to access the instance. In fact, doing something like this:


remove_action( 'shutdown', [ new SomeClass(), 'on_shutdown' ] );

will not work, because the instance of SomeClass passed to remove_action is not the same instance passed to add_action, which means spl_objec_hash will return a different value for them. The consequence is that their identifier will be different.

The lost opportunity

For this reasons, many developers consider a bad practice to use callbacks involving objects and they suggest to always use named functions or static methods.

I personally strongly disagree with such statement when plugins are written in object oriented way.

Moreover, I think that the fact some of the functions of plugins API work well with some callback types and not with other is a flaw of the plugin API. Something that was totally understandable in 2004 when PHP was transitioning from version 4 to 5, closures and invokable objects did not existed and OOP PHP was at its first steps. But 14 years later, in my opinion, that can surely be considered a defect that had been better to address long time ago, especially considering that a big rewriting of the plugin API internals have been released just one year ago (with WP 4.7), without even discussing the possibility to address the issue in some way.

Strategies to get around the Issue “Remove WordPress Hooks”

Design plugins that make use of dynamic methods and closures and still allow third party code to remove them is possible by using a few strategies.

Strategy #1: The “enabler” filter

When there’s an object that adds hooks, it is possible to provide a filter that acts as a flag to enable or disable the hooks addiction:


class SomeClass {
  
  public function initialise() {
    
    if ( ! apply_filters( 'some_class_enable', true ) ) {
      	add_action( 'init', function() { /* */ } );
        add_action( 'shutdown', function() { /* */ } );
    }
  }
}

With a class like this, third party code can prevent hooks to be added, instead of removing them:


add_filter( 'some_class_enable', '__return_false' );

This one liner not only is easy and short, it is also very readable and more explicit than multiple calls to remove_actions , being at any affect an API to disable class functionalities.

Moreover, it is better for backward compatibility. If the next release of SomeClass will add different hooks, by just keeping the 'some_class_enable' in place it is possible to keep compatibility with third parties that used to remove hooks: third parties don’t have to care what happen inside the if block.

Strategy #2: Expose the object via action

When objects add hooks, it is possible to equip them with methods that enable and disable functionalities, encapsulating the related hook operations inside those methods.

After that, the same methods can trigger actions passing $this as argument, so exposing the object to third parties which can make use of enable / disable methods.

An example:

class SomeClass {
  
  private $enabled = true;
  
  public function init() {
    
    add_action( 'init', function() {
      if ( $this->enabled ) {
        /* do something here*/
      }
    } );
    
    do_action( 'some_class_init', $this );
  }
  
  public function enable() {
    $this->enable = true;
  }
  
  public function disable() {
    $this->enable = false;
  }
 
}

When hooks are added in such way, third parties can do things like this:


add_action( 'some_class_init', function( SomeClass $some_class ) {
  $some_class->disable();
} );

This strategy, just like the previous, is explicit, readable, and very useful for backward compatibility. It brings major benefits when there are more methods to enable/disable features, allowing third parties to pick the exact features they need.

A variation of this strategy, make use of an external initializer that creates the class, add hooks and then expose the used instance:


class SomeClass {
  
  public function on_init() {
    /* */
  }
  
  public function on_template_redirect() {
    /* */
  }

}

function initialize_some_class() {
  
  $class = new SomeClass();
  
  add_action( 'init', [ $class, 'on_init' ] );
  
  do_action( 'some_class_init', $class );
}

In this variant enable / disable methods are not necessary because thirds party could directly remove hooks by calling remove_action / remove_filter:


add_action( 'some_class_init', function( SomeClass $some_class ) {
  remove_action( 'on_init', [ $some_class, 'on_init' ] );
} );

This approach favorites a cleaner design of the class, that does not have to deal with flags and related conditionals, but enforce all the methods used in hooks to be public (which might or might not be desirable) and is less flexible in term of backward compatibility: every time initialize_some_class function changes, third parties that make use of some_class_init hook very likely needs to change as well.

Strategy #3: Shared instance / registry pattern

The difficulty to remove hook callbacks that uses objects is to access the exact instance that is used. Pattern like shared instances and registry pattern allow to access specific object instances, and so third parties are enabled to remove hooks.

A very trivial example:

function some_class_instance() {
  static $instance;
  $instance or $instance = new SomeClass();
  
  return $instance;
}

add_action( 'init', [ some_class_instance(), 'on_init' ] );
add_action( 'shutdown', [ some_class_instance(), 'on_shutdown' ] );

third parties can do:


remove_action( 'init', [ some_class_instance(), 'on_init' ] );
remove_action( 'shutdown', [ some_class_instance(), 'on_shutdown' ] );

This approach can be even more powerful and backward compatible if the shared instance has a dedicated API for enabling / disabling functionalities, as seen in the strategy #2.

Please note that:

  • shared instances are not singletons
  • even if storing an object in a global variable is at any effect a shared instance pattern implementation, it is the worst implementation one can think of.

When everything seems lost…

The strategies shown above work well for new plugins you are going to work on, but many time developers need to deal with existent plugins they did not write.

In those cases, when in the need to remove a hook, the last-resort consist in:

  1. access the global $wp_filter variable, where WordPress stores both all actions and filters
  2. for every callback added using an object, check the class and the method used to find the one that matches the target
  3. remove it when found

even if this could work, it has 2 main issues:

  • it does not really work with closures: all closures are instance of the same class and have no methods, so when more closures are added to same hook at same priority, there’s no way to distinguish one closure form another
  • it requires some code that needs to be done again and again for every hooks the one wants to target (it does not scale!)

How to remove WordPress Hooks with “Object Hooks Remover”

Inpsyde just released a package named “Object Hooks Remover” that provides a last-resort solution to the issue of removing hooks which uses object methods or closures.

The package provides 5 functions:

  • Inpsyde\remove_object_hook
  • Inpsyde\remove_closure_hook
  • Inpsyde\remove_class_hook
  • Inpsyde\remove_instance_hook
  • Inpsyde\remove_invokable_hook

Each one has different use cases.

Inpsyde\remove_object_hook

This function is used to remove hook callbacks that use dynamic object methods.

The object methods to remove are identified by the class and method name. An hook added like this:


add_action( 'init', [ new SomeClass(), 'on_init' ] );

can be removed like this:


Inpsyde\remove_object_hook( 'init', SomeClass::class, 'on_init' );

When called like this, the function removes the callback no matter the priority (unlike remove_action / remove_filter) but it is possible to limit the priority to target passing it as third parameter.

Read more about this function.

Inpsyde\remove_closure_hook

Remove hooks using closures has always been incredibly tricky, because as previously said, it is hard to programmatically distinguish one closure from another.

The new Inpsyde package attempt to solve this issue, distinguishing a closure from another thanks to two characteristics:

  • the object the closure is bound to (the bound object is the object that resolves as $this inside the closure. It is set automatically to the object the closure is declared from but can be set explicitly, see Closure::bind and Closure::bindTo)
  • the closure signature (i.e. parameter names and types)

For example, a hook added like this:


class SomeClass {
  
  public function __construct() {
    add_action( 'init', function( $foo, $bar ) {
      /* */
    } );
  }
}

can be removed like this:


Inpsyde\remove_closure_hook(
  'init',
  SomeClass::class,
  [ '$foo', '$bar' ]
);

The bound object to target has been identified by its class name in the example above, but having access to the exact instance of the object that declared the function, it is possible to pass the object instance as second parameter limiting the match of closures to those added by that specific instance.

This function, just like remove_closure_hook (and unlike remove_action / remove_filter), removes matching closure(s) added to any priority, but it is possible to limit the priority to target passing it as third parameter.

Read more about this function.

Inpsyde\remove_class_hook

Similar to remove_object_hook this function only targets static methods. Even if static class methods could be removed via remove_action / remove_filter, this function can be still useful because can remove callbacks from any priority and even without specifying a method name).

Read more about this function.

Inpsyde\remove_instance_hook

Remove hook callbacks added with a specific object instance that must be provided.

When having access to the exact instance used by some hooks, it would be possible to remove those hooks via remove_action / remove_filter, but this function can still be useful because in a single call can remove all the hooks that use the instance, no matter the method or the priority used.

Read more about this function.

Inpsyde\remove_invokable_hook

No more than a shortcut for using Inpsyde\remove_object_hook passing __invoke as method name (second parameter).

Read more about this function.

More about the package

“Object Hooks Remover” is a Composer package, which means that any plugin, MU-plugin or theme which can require the package via inpsyde/object-hooks-remover can use (and reuse) it.

It requires PHP 7+ and it is licensed under MIT license.

And tomorrow there will be another great contribution to our Inpsyde WordPress Advent Calendar!

Comments

  1. Cameron1

    Wow, this looks like exactly what I need. While I am not truly a “Developer”, I understand WordPress and PHP enough to be dangerous. Having said that, I am not certain how to use your Composer package.

    Can you provide some quick instructions on how to “install/implement” these tools you have developed?

    Many thanks!

  2. Giuseppe Mazzapica2

    Hi Cameron,
    well if you’re familiar with Composer (https://getcomposer.org/) you can use Composer to install it, and you’ll need just the package name that is “inpsyde/object-hooks-remover”.

    In case you are not familiar with Composer, you can:

    1) create a folder that will host a new plugin
    2) go to the package repository that is: https://github.com/inpsyde/objects-hooks-remover and grab the 2 files that are in the “/inc” folder
    3) write the plugin file that contains the plugin headers (see https://codex.wordpress.org/File_Header) and require the 2 files from my package.
    4) Use package functions as explained in the article to do whatever you need to do 🙂

    An example for a plugin you could write this way can be found here: https://gist.github.com/gmazzap/4af64e1991edb715228c79b06689b580

    Of course, if you use the package this way, and the plugin is updated your plugin is not updated automatically and you would need to copy the 2 files again…

Leave a Reply to Cameron Cancel reply

Your email address will not be published. Required fields are marked *