Inside WordPress Actions And Filters

Advertisement

Gone are the days when WordPress developers, wanting to extend the CMS’ functionality, had to alter and hack WordPress’ source code directly, resulting in headaches when upgrading and sharing modifications. When WordPress 1.2 rolled out1 back in 2004, a new plugin architecture was introduced that is now commonly referred to as actions and filters, hooks, and the Plugin API.

WordPress Actions and Filters

WordPress’ core has been carefully sprinkled with actions and filters that external code (in the form of themes and plugins) can hook into, injecting new functionality into the standard flow. The Plugin API2 provides a neat interface to work with actions and filters. This article gathers insight into the inner workings, elegance and beauty of the Plugin API. It will help WordPress plugin and theme developers gain a more profound understanding of what happens behind the scenes, why some things will work and others won’t, and where to look when they unexpectedly don’t.

Warning

This is a detailed walkthrough of some of WordPress’ core source code. To get the most understanding, insight and fun from it, you will need basic knowledge of PHP and WordPress functions, as well as the source code for WordPress 3+3 (which can be viewed online4) and the courage to dig in and get your hands dirty.

The Plugin API

The functions that theme and plugin developers most commonly use are these:

These functions are well-known, well-documented, and used abundantly in a majority of themes and plugins. The Codex Plugin API9 page provides some basic examples of WordPress hooks in action. The Plugin API’s source code has made itself at home in the /wp-includes/plugin.php file. Feel free to open it in your favorite text editor or view it online10 to follow along.

The API is quite compact, around only 350 lines of code (the rest are comments). It exposes 22 functions, 14 of which work directly with actions and filters, while the rest are helper functions and utility functions that pertain to plugin path resolution, activation and deactivation.

The Plugin API is made available in the earliest stages of the WordPress boot-up process, and the earliest action one can hook into is muplugins_loaded, which is fired off after all “must use11” and network-wide12 plugins are included — rather useless if your plugin is neither of those. The plugins_loaded action is fired off immediately after all valid plugin files are included in the scope. Finally, after_setup_theme is fired off once the active template functions.php has been included.

The Actions Reference13 and the Filter Reference14 contain descriptions for many of the Actions and Filters available during typical request scenarios.

$wp_filter

The Plugin API provides functions that act on the $wp_filter global, which is a simple associative array with a particular structure. All of the action and filter functions read from and write to this globally shared associative array, which makes the API completely decoupled from WordPress’ core code. You can actually include the Plugin API’s plugin.php file in any other PHP project or framework and use all of the action and filter functions (do/apply, add, remove, has, current) without any modification whatsoever. In fact, an almost unchanged file is shipped with BackPress15, a collection of standalone libraries that grew out of WordPress. The secret to this lies in the high flexibility of the API and the simplicity of its concept.

The $wp_filter global starts out in WordPress as an undefined variable, completely void of any data. Data is written to it once an action or filter is added via add_action() or add_filter(). So, this will be our starting point. The two functions have an identical prototype in terms of function arguments:

function add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1)

This function is defined on line 6516. It’s very simple. Notice how it returns true quite regardless of what happens inside. The add_action() (on line 33117) function invokes the add_filter() function without any modification. This means that $wp_filter does not distinguish between filters and actions when queuing them. The data structure of $wp_filter is quite simple and can be represented by the following diagram:

WordPress filter structure18
The data structure of $wp_filter.

A “function” array (i.e. an array containing a function callback and the number of arguments it accepts) is identified by the _wp_filter_build_unique_id() (line 75019) helper function, which returns a unique idx for a callback, ordered by “priority” and attached to a “tag” (which is the name of the filter or action). This results in a list of actions with unique callbacks that will be invoked only once, regardless of how many times the same callback is added (the unique ID ensures this).

Come Get Some Action!

Actions are usually fired off via the do_action() function, which has the following prototype:

function do_action($tag, $arg = '', ...)

The definition of the function is on line 35920, you’re more than welcome to read the bits of code there and return for a step-by-step explanation.

First of all, the $wp_actions global keeps track of how times a particular action has been triggered. It’s a simple associative array, with the action tag or name as its keys. The did_action() function (line 42321) returns the number of times that an action has been triggered by accessing this $wp_actions array. Next, the all action is triggered by the _wp_call_all_hook() function. This function simply pulls on all of the registered or added hooks in the $wp_filter['all'] array using the PHP call_user_func_array()22 function (we will look at a great application of the all action a bit later). This is followed by a simple “check and return if action does not exist.”

Next, you’ll notice that the action tag is pushed into the global $wp_current_filter array. The current_filter() function (line 30623) returns the last value stored in this global array. Action and filter callbacks can pull on more actions and filters, invoking other callbacks, resulting in long chains. One can trace the chain of hooks by looking into the global $wp_current_filter array during the execution of a callback. However, these chains do not usually get any longer than a couple of links, unless you do this:

add_action( 'my_action', 'my_function' );
add_action( 'my_action_2', 'my_function_2' );
add_action( 'my_action_3', 'my_function_3' );
add_action( 'my_action_4', 'my_function_4' );
function my_function() { do_action( 'my_action_2'); }
function my_function_2() { do_action( 'my_action_3'); }
function my_function_3() { do_action( 'my_action_4'); }
function my_function_4() { var_dump( $GLOBALS['wp_current_filter']); }

do_action( 'my_action' );

/* array(4) {
[0]=> string(9) "my_action"
[1]=> string(11) "my_action_2"
[2]=> string(11) "my_action_3"
[3]=> string(11) "my_action_4"
} */

Why would anyone do this? The previous is an evident example, however consider the following piece of code involving the get_{$meta_type}_metadata where I want to augment a specific key:

add_filter( 'get_post_metadata', 'augment_post_meta_by_key', 999, 4 );
function augment_post_meta_by_key( $null, $object_id, $meta_key, $single ) {
  if ( $meta_key != 'my_key' ) return $null; /* Ignore everything else */
  /* Get the value */
  $value = get_post_meta( $object_id, 'my_key', true );
  if ( $value == '12345' ) return '54321'; /* Simple augmentation of a meta value */
}

Do you see the pitfall? Correct, this is an infinite loop. The filter will fire off inside of augment_post_meta_by_key because we’re doing more meta requests. So the $current_filter chain will be quite long if you look at it. A solution would be to remove the filter before getting the value and re-adding it afterwards.

Back on track: look at line 38624. All of the callback arguments are assembled into the local $args variable, through the use of PHP’s func_get_arg()25. If you’re curious about the unusual // array(&this) business around line 387, check Ticket #1711126.

Right after the callback arguments are taken care of, the $wp_filter global has its data for the current tag sorted by priority. When actions and filters are added to the tags in $wp_filter, priority arrays that contain the callbacks are created and pushed into the tag array in an unsorted order. In other words, adding four actions with priorities of 10, 1, 15, 3 would result in the tag containing priorities 10, 1, 15, 3 in the exact same order; thus, sorting by priority is required. Sorting is done by a simple ksort()27, and the global $merged_filters array keeps track of whether a tag’s priorities are sorted or not. The usage of ksort() shows that priorities can be strings and negative numbers, which is perfectly valid, and that no action callback is ever guaranteed to run first. When an action or filter is added, this line of code28unset( $merged_filters[$tag] ); makes sure that the priorities are sorted, even if they’ve been sorted once before.

Next, each $wp_filter[$tag] callback is invoked by the call_user_func_array()29 function, with the second argument (i.e. the array of arguments to call the actions with) truncated to the number of accepted arguments ($accepted_args).

Finally, the current action gets unset from $wp_current_filter.

Filters

The apply_filters() function (line 13430) goes through practically the same process as the do_action() function, with some minor differences in code implementation and a major difference in the fact that the apply_filters() function returns a value.

If you’ve been reading the source code, you may have noticed by now that has_action() is wrapped around has_filter(); that remove_action() and remove_all_actions() are wrapped around remove_filter() and remove_all_filters(); and that add_action() is wrapped around add_filter()

So, Why Bother!?

Although both will call your functions the same way, and you actually could — but never should! — apply add_action() to lists of filters and vice versa, or use apply_filters() instead of do_action() or even do_action() instead of apply_filters(), keeping them functionally and semantically separate is absolutely critical. As Samuel Wood says in “Actions and Filters Are Not the Same Thing31”:

Filters filter things. Actions do not. And this is critically important when you’re writing a filter. A filter function should never, ever, have unexpected side effects.

ref_array

A quick note about do_action_ref_array() (line 19732) and apply_filters_ref_array() (line 44833). These functions contain the same code as their non-ref_array counterparts, and they accept an array of arguments instead of a list of arguments:

do_action( 'my_action', 'a string', array( 1, 2, 3 ), false, 2 );
$my_action_arguments = array(
'a string',
array( 1, 2, 3 ),
false,
2
);
do_action_ref_array( 'my_action', $my_action_arguments );

The two behave the same. The array version is convenient to use when your arguments have been building up in an array, because you won’t have to unpack it.

Debugging

Dumping

A clean installation of WordPress will contain around 200 actions and filters and twice as many registered callbacks when the wp action fires off. You probably dumped the global $wp_filter array at the beginning of this article to see its structure, and perhaps noticed that interpreting it is quite difficult due to the massive amount of data and the var_dump presentation. Now that you’re comfortable with the structure of $wp_filter, the array can be custom pretty-printed34 with something more or less as simple as the following:

echo '<ul>;
/* Each [tag] */
foreach ( $GLOBALS['wp_filter'] as $tag => $priority_sets ) {
  echo '<li><strong> . $tag . '</strong><ul>;

  /* Each [priority] */
  foreach ( $priority_sets as $priority => $idxs ) {
    echo '<li> . $priority . '<ul>;

    /* Each [callback] */
    foreach ( $idxs as $idx => $callback ) {
      if ( gettype($callback['function']) == 'object' ) $function = '{ closure }';
      else if ( is_array( $callback['function'] ) ) {
        $function = print_r( $callback['function'][0], true );
        $function .= ':: '.print_r( $callback['function'][1], true );
      }
      else $function = $callback['function'];
      echo '<li> . $function . '<i>(' . $callback['accepted_args'] . ' arguments)</i></li>;
    }
    echo '</ul></li>;
  }
  echo '</ul></li>;
}
echo '</ul>;

The result is a more compact and friendlier report. Of course, when debugging, you’ll probably know what you’re looking for, so there would be no need to dump the whole $wp_filter.

A custom pretty print of the $wp_filter global vs. a var_dump35
A custom pretty-print of the $wp_filter global (left) compared to a var_dump (right).

Tracing via the “all” Hook

Remember the all hook (line 14036)? It fires off every time the apply_filters() or do_action() function is called. This means that tracing the execution of filters and actions is possible and quite useful for debugging.

/* Hook to the 'all' action */
add_action( 'all', 'backtrace_filters_and_actions');
function backtrace_filters_and_actions() {
  /* The arguments are not truncated, so we get everything */
  $arguments = func_get_args();
  $tag = array_shift( $arguments ); /* Shift the tag */

  /* Get the hook type by backtracing */
  $backtrace = debug_backtrace();
  $hook_type = $backtrace[3]['function'];

  echo "<pre>";
  echo "<i>$hook_type</i> <b>$tag</b>n";
  foreach ( $arguments as $argument )
    echo "tt" . htmlentities(var_export( $argument, true )) . "n";
 
    echo "n";
    echo "</pre>";
}

The little code snippet can be improved by adding timestamps for profiling, along with $wp_filter dumping to show more information about what has been called, and so much more useful stuff.

Final Thoughts

WordPress has evolved a lot since the early versions, and it is one of the best examples of how to write a CMS in PHP and other programming and scripting languages. WordPress’ core architecture has become very robust, and software engineers could learn a lot from the platform’s source code. The inner workings, elegance and beauty of WordPress’ actions and filters has given me (and hopefully you, too) tremendous insight, inspiration and motivation to keep digging.

More Resources

Don’t stop here! Your journey has just begun. Check these out:

  • WordPress Hooks Database37,” Adam Brown
    An overview of all actions and filters that are present in WordPress, with source-code locations and much more.
  • Debug WordPress Hooks38,” Andrey Savchenko
    More advanced hook-dumping snippets for WordPress.

(al)

↑ Back to topShare on Twitter

Gennady Kovshenin is a freelance web-applications consultant and developer from Russia. Finds WordPress as one of the most remarkable PHP frameworks and a fantastic CMS. Drinks lots of tea, has a degree in Linguistics, adores Linux, information security and everything programming. Runs a humble blog, sharing thoughts on application development and WordPress, and is at home on Twitter.

  1. 1

    Konstantin Kovshenin

    February 16, 2012 8:19 am

    Congrats on your first post, bro! Haven’t read it yet, but I’m sure it’s awesome! :)

    1
  2. 2

    What a great walk through! I’ve enjoyed reading this post and I have learned a whole bunch of new things about the hooks API. Thanks Gennady!

    2
  3. 4

    Hi Konstantin,
    great post about this topic; maybe a tipp for users, there will more find hooks and learn on the system; the plugin can help to do this, list different hooks and many more to find an hook for an solution.

    0
  4. 5

    Thanks… this make us more clearly about it..

    0
  5. 6

    Thanks for this great walk through. I learned a lot! Please do more of these “inside” articles!

    0
  6. 7

    Great article. Very helpful. I’m getting a deeper understanding of actions and filters.

    There were errors in your $wp_filter inspection code that prevented it from running. It looks like some single-quotes were missing.

    I’ve gotten it working:

    https://gist.github.com/sunilw/6968727

    I know the long list of ‘echo’ statements seems a bit odd. But there was an error in a long line that had a lot of quotes. The easiest way to track down the missing character was to break up that line.

    0
  7. 8

    Thanks Gennady for this great tutorial :)

    0

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top