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 out 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’ 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 API 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.
Further Reading on SmashingMag:
- Utilizing User Roles In WordPress
- How To Become A Top WordPress Professional
- Useful WordPress Tools, Themes And Plugins
- Writing Effective Documentation For WordPress End Users
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+ (which can be viewed online) 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 API 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 online 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 use” and network-wide 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 Reference and the Filter Reference 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 BackPress, 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 65. It’s very simple. Notice how it returns true
quite regardless of what happens inside. The add_action()
(on line 331) 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:
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 750) 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 359, 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 423) 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()
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 306) 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 386. All of the callback arguments are assembled into the local $args
variable, through the use of PHP’s func_get_arg()
. If you’re curious about the unusual // array(&this)
business around line 387, check Ticket #17111.
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()
, 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 code - unset( $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()
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 134) 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 Thing”:
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 197) and apply_filters_ref_array()
(line 448). 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-printed 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 (left) compared to a var_dump
(right).
Tracing via the “all” Hook
Remember the all
hook (line 140)? 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 Database,” Adam Brown An overview of all actions and filters that are present in WordPress, with source-code locations and much more.
- “Debug WordPress Hooks,” Andrey Savchenko More advanced hook-dumping snippets for WordPress.