Follow Jeff Sayre on Twitter

WordPress Hooks, Barbs, and Snags

By

This article is intended to help you learn how to interpret the output from the WordPress Hook Sniffer developer’s tool

This article is my exhaustive study of what I thought was a simple little function—the do_action function. It details how WordPress action hooks really work. It is a long, detailed article. If you want to understanding the inner works of the do_action function, then it will be worth your time. Although this article only briefly mentions the apply_filters function, the lessons learned about the do_action function apply equal as well as the coding of these two functions is nearly identical.

This is not a beginners guide to WordPress action and filter hooks. To benefit from this article, you need to understand what hooks are, why you use them, and how to use them.

Hooks, Barbs, and Snags

Before we start, I need to define two terms. You may be wondering about the title of this article, “WordPress Hooks, Barbs, and Snags.” You should already know the definition of a WordPress hook. But what are barbs and snags?

These are two terms I created to classify the following:

Barbs: salient insights about the functioning of WordPress hooks that stick out and grab your attention. Understanding these barbs help you avoid snags. In this sense, a barb is a good thing.

Snags: WordPress hooks that do not execute as expected, not because the do_action or apply_filters functions are misbehaving, but rather because you do not understand how these functions actually work.

The Hook Loop or Where the Issue Gets Complex

When it comes to code, loops can be tricky constructs to properly implement. What seems like a simple, innocuous operation may actually be more complex than meets the eye. This is the case with the do loops in the do_action and apply_filters functions.

There are two key arrays that WordPress uses to determine the firing sequence of actions and filters— the $wp_actions and $wp_filter arrays. These arrays get built throughout the execution of each page load and are not fully populated until page load is complete and PHP stops execution, waiting for its next command.

The $wp_filter Array

The $wp_filter array is where all added action functions and filter functions are stored for future reference and processing by both the apply_filters function and the do_action function. This array is a very-large, multidimensional array nested to four levels. See the snapshot of the partial contents of this massive array.

Here is a snapshot of what is generated in the $wp_filter array just by navigating to the homepage in my particular setup. Your results will vary depending on which plugins you have activated, which theme you’re using, whether you’re running WordPress in single-site or multi-site mode, whether you’re running BuddyPress, whether a user is logged into your site, and of course which versions of WP / BP you currently have installed.

As you can see, there are a lot of action functions and filter functions listed, each of which result in the grabbing and storing of added actions and filters. Not all of these are necessarily triggered. It is up to the apply_filters and do_action functions to determine which of the referenced functions will be fired.

The $wp_action Array

The $wp_actions array holds all of the current actions invoked by do_action events in the files that are processed when a given page is loaded. Like the $wp_filter array, the contents of this array obviously depend on the page to which a user navigates.

Here is a snapshot of what is generated in the $wp_actions array just by navigating to the homepage in my particular setup.

But read on as knowing about these two arrays is only one aspect to fully understanding the underlying processes, in figuring out the entire puzzle.

Populating the Arrays, Firing the Hooks

Here’s another piece to this complex puzzle. We know that the array $wp_filter holds an array listing all of the currently active action functions and filter functions. But how is this array built?

It’s simple. All action and filter functions that are not enclosed within a function, a loop, or any type of conditional clause currently not triggered, are added sequentially to this array as a given file is executed after it’s loaded (via either an include, include_only, require, or require_only function call). Therefore, as soon as a file is loaded, execution passes to that file and all directly executable add_action, add_filter, remove_action, and remove_filter function calls are processed.

This means that PHP is furiously processing function calls from these four functions, resulting in data being added or removed from the $wp_filter array. When an add_action call is triggered, the parameters are sent to the add_action function in the WP plugin API. When an add_filter call is triggered, the parameters are sent to the add_filter function—and so on.

However, one additional function is called when an add_action call is processed. As soon as the add_action function gets called, it immediately passes on the responsibility to the add_filter function. This means that WordPress looks at action functions and filter functions as the same. This is another key insight and is why both action functions and filter functions are referenced within the same array— the $wp_filter array.

Once the add_filter function gets control, it adds the calling function to the growing list of actions and filters that can be triggered by a given do_action or apply_filters event. This process quickly results in the $wp_filter array growing into a massive, multidimensional array. Of course, action and filter functions can get removed from the array via calls from the remove_action and remove_filter functions.

The reality of code execution is of course more complex. The $wp_filter array is still being populated with action and filter functions when the first do_action events are triggered. This means that it is possible to have a situation where an action or filter that is hooked to a given do_action or apply_filters event will not get triggered if the file in which the hooked function is located has not been loaded before the hook is triggered.

This last point is exactly what was happening in BuddyPress when the bp_init action hook fired. This is the issue that was puzzling me and resulted in this exhaustive study and creation of my new WordPress Hook Sniffer plugin. You can read this BuddyPress Trac ticket to learn more.

A Detailed Analysis to Shed Some Light

Here is a breakdown and partial analysis of the $wp_actions array, the array that holds all of the current actions invoked by do_action events in the files that are processed when the homepage is loaded. We will use this array along with the $wp_filter array to figure out what is really going on.

The contents of this array obviously depends on the page to which a user navigates. This is just what happens when a non-logged-in user visits the homepage of the default BuddyPress theme in my WPMU setup. The only other plugin that is activated is my BuddyPress Privacy Component. For these tests, I am running WPMU 2.9.2 and BuddyPress 1.2.3.

NOTE: To fully understand what is going on, you need to make sure that you are looking at the completed arrays and not the partial arrays grabbed partway through page-load.

Let’s look at the order in which do_action events will be triggered. We will analyze what is happening in the first four elements of the $wp_actions array (index value of “0” through “3”). How is each action triggered and what are the results? This will start off as a detailed look into each hook’s actions, but will provide fewer details with each additional event as the basic processes become evident.

The First Four Triggered Action Events

[0] => muplugins_loaded

[1] => bp_core_loaded

[2] => plugins_loaded

[3] => bp_setup_root_components

[0] => muplugins_loaded

When a user navigates to the homepage of a default BuddyPress install running on top of WPMU, the first action hook that is invoked is muplugins_loaded. This is directly triggered on line 547 in wp-settings.php. Any added actions that are tied to this event will next be triggered.

Looking at the $wp_filter array, you will see that there are no actions tied to this hook. This means that there is no subarray element with a key name of muplugins_loaded. In other words, you will not find any reference to that action event within the $wp_filter array. So, this action event does not result in any action functions getting fired.

NOTE: A triggered hook may or may not have actions tied to it. If no actions are tied to the hook, the do_action routine passes by that hook, checking the next invoked hook for added action functions to fire.

Before the next action event is triggered, something important happens.

Execution of the wp-settings.php file continues. On line 703 of wp-settings.php, all the active plugins are loaded—both blog specific plugins and site-wide plugins.

If you look at that line, $current_plugins can be modified by any added filters. That is what happens. On line 2348 of wpmu-functions.php, an add_filter call triggers function mu_filter_plugins_list. This filter modifies the contents of $current_plugins, adding any MU plugins that have been activated site wide. Processing is returned wp-settings.php, resulting in the main file of each activated plugin getting loaded.

There is an interesting item to notice within function mu_filter_plugins_list. On line 2345 of that function, the two arrays $active_plugins and $active_sitewide_plugins are merged and the resultant new array is sorted.

What does this mean? It means that when it comes to the issue of action and filter sequence firing, it does not matter which plugins you activate first. The order in which plugins are added to each of the serialized objects active_plugins and active_sitewide_plugins is immaterial. The sort function will alphabetically reorder the contents of the merged array.

NOTE: Each WPMU blog has it’s own listing of active plugins that are stored in a serialized object in the blog’s wp_x_options table. Look for the entry “active_plugins” within the option_name field of that table. The corresponding active plugins are in the option_value field of that record. All plugins that are to be activated site wide within WPMU are stored in a serialized object in the wp_sitemeta table. Look for the entry “active_sitewide_plugins” within the meta_key field of that table and the corresponding plugins that are to be activated site wide in the meta_value field of that record.

So, in our particular case with two site-wide plugins installed and activated (buddypress and bp-authz), the order in which they were activated is immaterial in determining which file is included (loaded) first by the conditional loop code starting on line 704 of wp-settings.php.

If you look in the wp_sitemeta table for the meta_key “active_sitewide_plugins”, you will see this:

a:2:{s:24:"buddypress/bp-loader.php";i:1270239897;s:28:"bp-authz/bp-authz-loader.php";i:1270396554;}

BuddyPress appears first because I activated it first—as is necessary. But, when the references to these two main plugin files are processed through the mu_filter_plugins_list function, they get sorted. This results in bp-authz-loader.php getting loaded before bp-loader.php.

This could be a big issue as all BP-dependent plugins rely on BuddyPress to be installed, activated, and loaded. Any BP-dependent plugin that comes alphabetically before “buddypress” will have their main file loaded before BuddyPress’ main file.

Barb 1: As a BuddyPress plugin developer, this is the first major insight, the first important barb that can prevent your code from hitting a snag with regards to action and filter execution.

Fortunately, there is an easy way to prevent this barb from snagging you and those that use your plugin. Make sure that BuddyPress is active before your plugin loads its core files.

Since that is exactly what I’ve done with my BuddyPress Privacy Component, there are no issues and execution quickly passes back to the loop and the next file in the $current_plugins array is loaded. This is the main BuddyPress file, bp-loader.php.

[1] => bp_core_loaded

Execution of bp-loader.php begins. On line 22 of bp-loader.php, the first custom BuddyPress action event is invoked:

do_action( 'bp_core_loaded' );

It is directly invoked, directly triggered. It is not enclosed within a function, a loop, or any type of conditional clause. This invoked event is what appears in the second element of the $wp_actions array.

Since there are currently no tied-in actions to this particular action event (as can be easily determined by looking at the $wp_filter array), the invocation of this action does not result in further actions being triggered.

The execution of bp-loader.php continues. Right after line 22, a series of conditional clauses include (load) all enabled BuddyPress modules—activity, blogs, forums, friends, groups, messages, xprofile.

As each of these files are included (loaded), any directly invoked (directly triggered) action events within each of them will also be invoked. Since there are no additional, directly invokable do_action calls within any of these files, execution is passed back to wp-settings.php.

[2] =>plugins_loaded

On line 735 in wp-settings.php, the do_action event for plugins_loaded is directly triggered.

do_action('plugins_loaded');

This is when things really start to accelerate. Up until this time, only a few lines of php code within BuddyPress have actually been run.

With the invocation of the plugins_loaded event, we now have our first action hook that has actions tied to it. Understanding what happens next leads us to the second major insight, the second important barb.

To understand what happens next, we need to inspect the elements within the plugins_loaded array of the $wp_filter array. I have extracted just that subarray element below:

plugins_loaded:
Array
(
[0] => Array
(

[wp_maybe_load_widgets] => Array
(
[function] => wp_maybe_load_widgets
[accepted_args] => 1
)

[wp_maybe_load_embeds] => Array
(
[function] => wp_maybe_load_embeds
[accepted_args] => 1
)

)

[2] => Array
(
[bp_core_setup_root_uris] => Array
(
[function] => bp_core_setup_root_uris
[accepted_args] => 1
)

[bp_setup_root_components] => Array
(
[function] => bp_setup_root_components
[accepted_args] => 1
)

)

[3] => Array
(
[bp_core_set_uri_globals] => Array
(
[function] => bp_core_set_uri_globals
[accepted_args] => 1
)

)

[5] => Array
(
[bp_core_load_buddypress_textdomain] => Array
(
[function] => bp_core_load_buddypress_textdomain
[accepted_args] => 1
)

[bp_setup_globals] => Array
(
[function] => bp_setup_globals
[accepted_args] => 1
)

)

[10] => Array
(
[bp_setup_nav] => Array
(
[function] => bp_setup_nav
[accepted_args] => 1
)

[bp_setup_widgets] => Array
(
[function] => bp_setup_widgets
[accepted_args] => 1
)

[bp_register_activity_actions] => Array
(
[function] => bp_register_activity_actions
[accepted_args] => 1
)

[bp_loaded] => Array
(
[function] => bp_loaded
[accepted_args] => 1
)

)

)

This array element is itself a multidimensional nested array. In fact, the first element (with an index value of “0”) has two action functions referenced.

An important point: each subarray of the $wp_filter array is a numeric array. Read that again. This means that the second level of the nested $wp_filter array contains numeric arrays.

You might be wondering how the index values for these numeric arrays are set. This is simple.

The key, the index value, of each 2nd-level array element is determined by the priority assigned to the event in the add_action and add_filter call. Thus, the priority set for a given action or filter function becomes the key of each element in a given hook’s multidimensional array. If a priority is not set for a given add_action or add_filter call, it is automatically defaulted to “10”. This means that any and all added actions or filters that do not specify a priority are automatically assigned to the proper action array element with an index value of “10”.

As an example, if the priority for a given added action is set to “16”, then that particular action function will be located in the array element for that action with an index value of “16”. Multiple action functions can be added under the same index value, creating an array of action functions associated with the same numeric key element. The order in which they appear is determined by the order in which they were executed.

Barb 2: This is the second major insight, the second important barb that can prevent your code from hitting a snag with regards to action and filter execution.

Now, the next piece of the puzzle is this. The order in which action functions are added to a given action array element is determined by the order in which they are encountered, the order in which their corresponding add_action or add_filter calls were fired. As we will see below, this is what makes determining the firing order of added actions and filters difficult.

Barb 3: This is the third major insight, the third important barb that can prevent your code from hitting a snag with regards to action and filter execution.

Barb 4: The higher the index key, the later the action or filter functions within that index-key grouping will get fired. Therefore, using the example above, if we set a priority of “16” for a given action function, it would get fired only after all the other action functions with a lower priority have been fired.

We have now reached the final piece of the puzzle!

Near the beginning of this article, the complexity of the do loops within the do_action and apply_filters functions was briefly mentioned. And it is within those loops that this story really finally gets resolved.

If you look at lines 166 (within the apply_filters function) and line 339 (within the do_action function) of the wp-includes/plugin.php file, you will find the code lines where the actual hooked functions are fired. Do you see the call_user_func_array() function call? That is a core PHP function. It is what will actually fire the next action or filter function in line, as determined by the data in the $wp_filter array.

The action of the call_user_func_array() function nested within the do loop is not as simple as it may seem. Why is this? Well, currently we are discussing the plugins_loaded action event and all of the tied into functions that get fired.

Look at the next action event to get triggered within the $wp_actions array. It is the action event bp_setup_root_components. But wait. Why is there another action event being triggered? We have not yet finished iterating through all of the added action functions to the plugins_loaded action event.

Once again, the reason is simple. Triggered action functions can themselves have action events. Inspecting the plugins_loaded subarray in the $wp_filter array, we see that the bp_setup_root_components action function is invoked by the plugins_loaded event via an added action call on line 2021 in bp-core.php. Within that action function there is an action event called bp_setup_root_components.

So, the sequential processing of all the added action functions with the plugins_loaded subarray of the $wp_filter function has come across a new action event to trigger. It now gets priority and further processing of the plugins_loaded subarray is put on hold until the bp_setup_root_components action event is finished processing its added action functions.

Barb 5: This can quickly result in a nested grouping of action events with the firing of their corresponding action functions. This is the final piece to the puzzle and the last and final barb that can prevent your code from hitting a snag with regards to action and filter execution.

As you are reading this, the juxtaposed actions of the do loop with the call_user_func_array() function more than likely seem obvious. It may seem this is not unexpected behavior or difficult to understand.

Whereas that is the case when all of the data is clearly laid out before you, the complex actions of these intertwined functions are not apparent when you are coding a plugin. There is no practical way to peek inside this action-event, action-function feedback loop.

This is were the true value of the WordPress Hook Sniffer plugin is revealed.

[3] => bp_setup_root_components

Here’s where we understand the last piece to this confusing puzzle.

Inspecting the $wp_filter array for this action event you will see that there are four action functions that are hooked to this action event: bp_activity_setup_root_component, bp_blogs_setup_root_component, bp_forums_setup_root_component, groups_setup_root_component.

These action functions are fired in that sequence. Since they do not have any action events which they trigger, control is passed back to the plugins_loaded event and the next action function in the plugins_loaded subarray is fired.

Applying the Lessons

When interpreting the results of the WoodPress Hook Sniffer plugin, you need to look at the output of the $wp_actions array and then search out the hooked actions to each of those items (if any) in the $wp_filter array. This will give you a general idea of what gets fired when. You can then experiment and see how changes to action priorities affect the firing sequence of a given added action.

You need to carefully think about the execution timing of each action or filter you add, looking at the corresponding triggering event for the hook or hooks to which it’s tied. Remember, you can hook a function to more than one hook which means that it is possible, if you are not careful, that some of them may fire whereas others may not. It’s all a matter of whether or not the reference to the action or filter function was successfully added to the $wp_filter array before the associated hook is triggered.

Looking at the Action Event Firing Sequence and the Filter Event Firing Sequence will help you precisely tune your added hooks and filters. Remember that execution my temporarily pass to another action or filter event. The output of these two WordPress Hook Sniffer arrays will show you when that happens.

Finally, there is no requirement that you place your add_action and add_filter calls right below the functions in which they are associated. Whereas doing so does facilitate code understanding, you could instead place them all at the top of the file (or the bottom) in which the associated functions are located. Remember, each add_action and add_filter function call that is directly executable is processed as soon as a file is loaded. So it does not matter where within the file those calls are located—with the possible exception of fine tuning the execution order of a particular function.

Final Notes on the $wp_filter and $wp_actions Arrays

The arrays $wp_filter and $wp_actions are repopulated from scratch each time a page is loaded. They do not contain a running accounting of all action and filter functions and all action and filter hooks. The contents of both of these arrays may vary depending on which page you navigate. The contents will depend on the files that are loaded (via include, include_only, require, require_only) when a given page you are on is displayed.

The reality is that, when visiting a given page, there will always be more hooks, action, and filter functions buried deep inside non-loaded files or within function blocks that are not directly hooked. The first is a result of the add_action, remove_action, add_filter, or remove_filter calls not getting fired until that file is loaded. At file load, any directly-executable do_action or apply_filters will be triggered as well. The second is the result of functions that have hooks that only get triggered if the file is loaded and the function is fired.

Although the WP Hook Sniffer will locate all action and filter hooks, and action and filter functions no mater the source–core files, custom themes, or 3rd-party plugins–what is captured depends on one simple rule—that the hooks and function calls are actually fired during page load.

As an example, WP Hook Sniffer cannot locate add_action calls that get fired on a different page than the currently loaded page. It can also not locate add_action calls that are buried inside a function that does not get triggered unless a special criteria is met. To sniff out those added actions, you have to navigate to that page so that those calls get fired or cause the special criteria to be met so that the function containing the buried action call is triggered.

Remember, the way that the WordPress Plugin API processes added and removed actions and filters is by storing (or deleting) a reference to them in the $wp_filter array. But that only happens for hooked functions that are actually encountered during page load. The $wp_filter array is wiped clean with each page load and rebuilt from scratch. The same holds true for the $wp_actions array and the action hooks it tracks.

Therefore, if you have action or filter functions that are supposed to be fired on a given page but you are not seeing that they’ve been fired, then that indicates that they are hooked to an action or filter event that finishes firing before the file in which the target action or filter functions are located even gets loaded. This is exactly what happened to me and caused me to write WP Hook Sniffer.

Summary of Barbs: the Key Insights

1. When it comes to the issue of action function and filter function sequence firing, it does not matter which plugins you activate first.

2. The key, the index value, of each 2nd-level array element within the $wp_filter array is determined by the priority assigned to the event in the add_action and add_filter call.

3. The order in which action functions and filter functions are added to a given action or filter subarray element in the $wp_filter array is determined by the order in which they are encountered during code execution.

4. The higher the index key, the later the action or filter functions within that index-key grouping will get fired.

5. Action hooks (events) –> trigger action functions –> which can have additional action hooks –> which can trigger additional action functions –> until control is passed back to the the originally-triggering action hook.

This is the action-event, action-function feedback loop caused by the juxtaposed actions of the do loop with the call_user_func_array() function.

Article Comments

  1. Dave Doolin says:

    Thanks for posting this on your blog instead of the codex. So much of the codex seems out dated, and I have too many times seen people with legitimate questions be treated very rudely.

    It’s my belief when we take the time to post on our own websites, we take more care with the material, and are more likely to keep it up to date.

  2. Jeff Sayre says:

    Dave-

    Thanks for the comment. I agree completely. I do not understand why developers take so much time coding a plugin and then simply pass it off to some other site. I understand hosting via the WP Plugin Repo but would think that they would want to discuss it and promote it via their own WordPress-based website.

  3. Boone Gorges says:

    Jeff, I just posted as much on Twitter, but I thought I’d also comment here: I think this essay is a real service to the WP developer’s community. Just the other day I was banging my head against the desk over something that I now sense was a firing-order issue. I have a feeling I’ll be coming back to this article multiple times in the future. Thanks.

    • Jeff Sayre says:

      You’re welcome, Boone.

      As I stated in this article, hook firing sequence recently caused me much consternation. Of course, initially I had no idea what was causing the issue. When hooks don’t fire in the proper sequence (or simply fail to fire at all) there is nothing in any log file that gives you a clue as to what went wrong. It took me awhile to figure out that it was a hook. When I did, that forced me to really understand what went on under the hood of the WP Plugin API.

      Thanks for the nice comment.

  4. [...] WordPress Hooks, Barbs and Snags – interpret the output from the Hook Sniffer tool [...]

Leave a Reply

Share on Twitter
Share on Facebook
Share on FriendFeed
Share on LinkedIn
Share on StumbleUpon
Share on Digg
Share on Delicious
Share on Technorati
Add to Google Bookmarks

Archives