Make WordPress hooks a tiny bit nicer to use
Whenever I find myself helping with a WordPress project, one of the first things I reach for is a set of functions I created to make hooks just a little bit nicer (less painful) to work with than add_action() and add_filter().
The pain is real
The hook system makes WordPress so flexible, but at the same time it makes it so difficult to understand how your website works – the opposite of good software design. Hook callbacks can be registered anywhere in the code, and at any time, so you never know what is hooking in where or when, and what side effects there might be… but I’ll resist the urge to turn this into a rant.
Unfortunately there is not much we can do about the underlying design. But we can make some small quality of life improvements to the interface we use when registering callbacks, by adding a thin convenience layer on top of add_action() and add_filter().
It started with those pesky accepted_args
It’s so easy to forget to set the number of arguments your function accepts, or to set it incorrectly, when registering a callback.
add_action('save_post', function ($id, $post) { // ❌ $post is not passed in because the accepted_args // parameter was not set, and a fatal error is thrown }); add_action('save_post', function ($id, $post) { // ✅ $post is passed in as expected }, accepted_args: 2);
Maybe those who work in WordPress full time don’t have this problem? But knowing about this common mistake hasn’t stopped me from making it time and time again. And each time I’ve found myself thinking, “It’s ridiculous that we even have to specify this number!”
on()
That leads us to on(), a simple wrapper on top of add_action().
With on() you can register a callback without having to set accepted_args.
on('do_a_thing', function ($id, $post, $whatever) { // it just works });
When setting a priority, you do that before the callback rather than after, so that it’s not easily missed when reviewing code.
on('do_a_thing', 20, function () { // });
Sometimes you need to register the same callback for more than one hook. Of course, the conventional WordPress way is to create a named function and call add_action() for each hook. But I prefer to use anonymous functions for hook callbacks, so it is clear what is a hook callback and what is a function that can be used in multiple places throughout your code.
To register a single callback for multiple hooks, you can pass in an array of hook names.
on(['wp_ajax_something', 'wp_ajax_nopriv_something'], function () { // });
filter()
filter() is a wrapper on top of add_filter() and works the same way as on().
filter('filter_a_thing', function ($value, $post, $whatever) { // return $new; }); filter('filter_a_thing', 20, function ($value) { // return $new; }); filter(['filter_a_thing', 'filter_another_thing'], function ($value) { // return $new; });
once() and filterOnce()
once() and filterOnce() will unregister the callback after it has run once. Needing to do this is probably a code smell, a sign of bad design. But when wrangling third-party plugins, you do what you have to.
once('do_a_thing', function ($id, $post) { // this will only be called once }); filterOnce('filter_a_thing', function ($value) { // this will only be called once return $new; });
This can be useful when you want some code to run on a second hook that is conditional on, or dependent on, what happens in the first hook.
on('first_action_name', function () { $data = getSomeDataUniqueToThisState(); once('second_action_name', function () use ($data) { // do something specific to $data }); });
Oh, the crazy things one must do when going to battle with WordPress plugins!
remove()
remove() is a wrapper on top of remove_filter().
remove('do_a_thing', 'standard_do_a_thing_callback'); remove(['do_a_thing', 'do_another_thing'], 20, 'other_callback'); remove('filter_a_thing', 'standard_filter_a_thing_callback');
Unlike remove_filter(), it can remove a callback registered on an object instance using just the class and method name, so you don’t need access to the original object. This is often useful when you need to reimplement a plugin’s callback.
remove('do_a_thing', [ThirdPartyPlugin::class, 'callbackMethod']);
Show me the functions!
You can find these functions in this gist.
There is also has(), a wrapper for has_filter(), and listCallbacks(), which can be useful when debugging.
All these functions are only a thin layer on top of the standard hook functions, and yet I’ve found them to be a good quality of life improvement – making errors less likely, and the code simpler and easier to read. They are proof that WordPress could easily improve its APIs while still retaining backwards compatibility, if developer experience and code quality were prioritised.
If you’re battling hooks, I feel your pain, and I hope this helps a little.