Registering callbacks with variable-name WordPress hooks

Some action and filter names used by WordPress and plugins have variable names like this one:

$result = apply_filters( "get_cached_value_for_{$key}", $value );

If you only care about one specific value of $key or the value is fairly predictable, that’s pretty OK (although still has the drawback of reduced searchability). If, however, you would like to register a callback for any value of $key, it might be problematic.

Luckily, we can work around these cases by using a special WordPress hook 'all'. This is a hook that runs whenever any hook runs.

Let’s assume our problematic filter has the name of get_cached_value_for_{$key} and look at this example:

<?php

add_filter( 'all', 'my_get_cached_value_filter' );

/**
 * @param string $hook_name Name of the hook that is currently firing.
 * @param mixed  $result Result returned from the hook.
 */
function my_get_cached_value_filter( $hook_name, $result ) {
	/*
	 * If the currently-executing hook does not start
	 * with "get_cached_value_for_", bail early by
	 * returning $result without modification.
	 */
	if ( 1 === preg_match( '/^get_cached_value_for_(.+)/', $hook_name, $matches ) ) {
		return $result;
	}

	$cache_key = $matches[1];
	echo "The filter was called for key = $cache_key.<br>";

	return $result;
}

Using variable-name hooks – an anti-pattern?

In my opinion, using variable-name hooks is an anti-pattern. I have never seen an example where doing this would improve anything but have had to come up with workarounds like the one above because of it.

A cleaner approach

Although with appropriate documentation this issue can be mitigated (if you know what hook names can be expected), why use this type of hook name when you can avoid them altogether? For example, instead of:

$result = 123;
$result = apply_filters( 'get_cached_value_for_{$key}', $result );

You could do:


$result = 123;
// Notice we pass `$cache_key` alongside `$result`.
$result = apply_filters( 'get_cached_value', $result, $cache_key );

// An example of registering a callback for the above hook format:

add_filter( 'get_cached_value', 'my_get_cache_callback', 10, 2 );

function my_get_cache_callback( $result, $cache_key ) {
	echo "The filter was called for cache_key = $cache_key.<br>";

	return $result;
}

This way:

  • the hook name is easily searchable in the codebase,
  • you don’t have to worry about keeping documentation up-to-date (although you probably should anyway :)),
  • people wanting to use this hook can support all possible values of $key or skip some using an if() condition if they want to,
  • there is a performance gain (even if very slight) because you have to only run the callback when a specific hook is called, not every time any hook fires because of registering the callback in the 'all' hook.

An alternative (less clean) approach

Another solution would be to use two separate hooks which is something done in some places in WP:

/**
 * Fires after a specific option has been deleted.
 *
 * The dynamic portion of the hook name, `$option`, refers to the option name.
 *
 * @since 3.0.0
 *
 * @param string $option Name of the deleted option.
 */
do_action( "delete_option_{$option}", $option );

/**
 * Fires after an option has been deleted.
 *
 * @since 2.9.0
 *
 * @param string $option Name of the deleted option.
 */
do_action( 'deleted_option', $option );

However, if you register a callback with the hook delete_option_title and if someone tries looking for that hook name in the codebase, they are not going to find it.

For the sake of searchability and simplicity, I prefer using an example like the one in the “A cleaner approach” section, even though it forces you to add a condition checking the value of the $key (or $option in the above example) argument.


Featured image note: “Hello my name is” by maybeemily is marked with CC BY-NC-SA 2.0.

Comments

Leave a Reply