Let’s say that you ran a poll through the WordPress developer community. It asked the following question: “What defines WordPress?”. The odds are that the plugin API would come up near the top.
From a developer’s perspective (and for the sake of my introduction), there’s nothing that defines WordPress like it. It’s the workhorse of the WordPress ecosystem. You use to build anything that you want with WordPress.
There are plenty of articles out there that show you how to use it, but little on how it works. That makes it a bit misunderstood and lonely (APIs have feelings you know).
It’s time we showed some love for this hard worker.
Actions vs Filters
The first thing to look at is the concept of Actions and Filters. It’s an important aspect of the plugin API. Actions and Filters serve two different purpose for WordPress. That said, they leverage the same system within it.
Their purpose
Let’s start with the purpose of the “action hook”. When WordPress calls an “action hook”, the plugin API notifies anyone interested in it. Those interested parties can then decide if they’d like do something or not.
That something always relates to how WordPress works. Someone might want to add new functionality to WordPress. Someone else might want to modify an existing one. But altering the behaviour of WordPress is always their goal.
Meanwhile, the intention is different when the plugin API applies filters. It still signals everyone, but it’s not to add new behaviour. It’s to change a piece of data.
This whole interaction process is a lot like having a polite conversation with WordPress.
Under the hood
So while the two have different purposes, you can’t say the same under the hood. There’s little that distinguishes the two. That’s because they use the same underlying system and a lot of the same code.
The system in question is WordPress filters. It’s a system that predates WordPress. It existed way back in the b2 days.
WordPress core developers added actions later in version 1.2.0. It wasn’t a new system built from scratch. No, core developers just built the action system on top of the existing filter system.
Now, that we spoke a bit about actions and filters. Let’s dig into the plugin API itself. Let’s look at how it works.
How does the plugin API work?
The plugin API needs four global variables to do its job. They store the API’s internal data inside associative arrays. Each array uses “hooks” for their keys.
Each API function interacts with these global arrays. It’s how they control it. In fact, it’s a form of object-oriented programming.
So what are these four global variables?
merged_filters
You have merged_filters
. It’s a flag that tracks if the plugin API sorted all the registered functions by priority. It only sorts them whenever you try to use a hook.
This can help WordPress’s performance quite a bit. It ensures that it only sorts the registered filters (an expensive operation) when necessary.
wp_actions
wp_actions
is an array of counters. Each time you call an “action hook”, the plugin API increments the counter for that hook. It also uses the hook as the array key used to store its counter.
So what’s considered a call to use an “action hook”? It’s whenever you use do_action
or do_action_ref_array
. These two functions will cause the counter for a hook to increment.
wp_current_filter
wp_current_filter
keeps track of the current hook the plugin API is processing. This isn’t a simple task. That’s because one hook might call another, that one might call another and so on. There’s no limit to how many hooks can chain from your initial call.
Storing the current hook in a string variable isn’t enough to handle this problem. You need to use something else. The solution is to use an array as stack. It’s a data type that implements the idea of “last in, first out” (or LIFO).
The image above shows how it works. When you make a call to a hook, the plugin API pushes it on top of the stack. The hook on top of the stack is the current filter or action. Once the call to the hook returns, the plugin API then pops it off.
If a hook function calls another hook, that hook will get pushed on top of the stack. It becomes the new current hook. It’ll also get removed before your initial hook because its call will finish first. That’s why it’s called “last in, first out”.
wp_filter
wp_filter
is where the plugin API stores every registered hook. It uses a large associative array to do it. The structure of the array is worth exploring. The graphic below shows you how the plugin API fills the wp_filter
array.
The array uses three keys to store the registered function array. These are the hook name, the priority of the registered function and a unique ID. Let’s take a moment to go over each of these elements.
Hook name
The first array key is the name of the hook that you want to register. The plugin API needs that for almost all its operations. So it makes sense that’d be the first key used.
Priority
Next is the priority. The plugin API needs to know the order to run functions for a given hook. It’ll go through hook priorities from the lowest number to the highest.
To understand why it works that way, we have to go back to the merged_filters
variable. merged_filters
exists because PHP doesn’t sort associative arrays by default. It just appends new keys to the array as it receives them.
So let’s say you had an empty hook with no registered functions. You then registered three functions with priority 50, 99 and 10. Without merged_filters
, the plugin API will call them in that order. That’s not what you want though. You need it to call the priority 10 function, the priority 50 and then the priority 99.
if (!isset($merged_filters[$tag])) { ksort($wp_filter[$tag]); $merged_filters[$tag] = true; }
The plugin API solves the issue by sorting the array using ksort
when the merged_filters
flag isn’t set. This sorts all array keys from lowest to highest. And that’s why hook priority works in that order.
Unique ID
The last key is a unique ID built with _wp_filter_build_unique_id
. If you’re registering a PHP function with your hook, the plugin API will use its name as the unique ID. That’s because you can’t have more than one function with the same name.
Things are a bit trickier if you use classes. While there are many ways to register a class method with the plugin API, it always comes down to two scenarios.
You want register an object method or a static method. In both cases, you’re always using an array with two values.
With an object method, your array will have an object and a method name. The plugin API can’t use the class name for that object. That’s because you might register it more than one object of the same class. To prevent this issue, your object needs a unique ID that the plugin API can use.
That’s where spl_object_hash
comes in. _wp_filter_build_unique_id
uses that function to create that unique ID for your object. It then appends the object method name to it.
Here’s an example of an object method ID built with _wp_filter_build_unique_id
: 0000000029917a6d000000000e5345b3method_name
. 0000000029917a6d000000000e5345b3
is the unique object ID returned by spl_object_hash
. method_name
is the object method name registered with the hook.
Things are a lot simpler with a static method. By definition, a static method is unique to a class. _wp_filter_build_unique_id
doesn’t need to use spl_object_hash
to generate the unique ID. It just builds it to match the static method call. It’ll always return an ID that follows this format: ClassName::method_name
.
Registered function array
The registered function array stores the necessary information to call the function you’re registering. It has two keys: function
and accepted_args
.
function
is the function you want the plugin API to call. It can be a string, an array or a closure. What’s important is that it needs to be a valid callback. A callback is a PHP pseudo-type that represents a function or object method that PHP can call.
accepted_args
represents the number of arguments that your function needs to receive. It’s used when the plugin API calls your registered function. It ensures that it passes the correct number of arguments to your function.
What can you do with the plugin API?
Ok, now that you’ve seen the global variables that lie beneath the plugin API. You have a better understanding of what powers it. The next step is to look at what it’s responsible for.
We’re not going to take a separate look at actions and filters. Instead, we’ll focus on the tasks that you want to do with it. This isn’t a conventional way to look at it, but it’s more practical.
This goes back to what you read earlier. There’s little that distinguishes actions from filters inside the plugin API. That’s why we’ll look at it from that angle instead. We’re also going to use the generic term “hook” to describe them.
Register a hook function
Registering a hook function is the most common thing that you’ll do with the plugin API. It’s done through either the add_filter
or add_action
function. These are the two functions that we associate the most with the API.
The functions themselves are identical. add_action
doesn’t do anything besides call add_filter
with the same four parameters. Those parameters are: tag
, function_to_add
, priority
and accepted_args
. add_filter
uses these parameters with the wp_filter
array that you just saw earlier.
function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1) { return add_filter($tag, $function_to_add, $priority, $accepted_args); } function add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1)
The first one is tag
. It’s a bit of a misleading variable name. It’s the name of the hook that you’re registering a function to.
function_to_add
is that function. It’s the callback stored in the wp_filter
array. The plugin API stores it under the function
key inside the registered function array.
priority
represents the importance of the function you want to register. As we saw earlier, the plugin API uses that value as an array key in wp_filter
. A lower number means that it’ll call these functions first. The default value is 10
.
Now, let’s say you have more than one function that share the same priority
. The plugin API will call them in the order that plugins and themes added them. If you need a function to run before another, you should give it a lower priority number.
accepted_args
is the number of arguments that function_to_add
wants to receive. The plugin API stores it with accepted_args
key inside the registered function array. It The default value is 1
.
Process hook functions
Now that you registered functions, let’s look at the other side of the equation. That’s processing all the functions registered to a specific hook. This is the most important and complicated thing that the plugin API does. It uses every global variable shown to you earlier.
We’re going to go over the process itself. This will give you a high-level view of what goes on. We’ll finish off by looking at the technical details of the four functions you can use.
Process overview
There’s a general pattern to how the plugin API processes hook functions. It can explain almost everything that goes on in all the related API functions. The rest are just small variations. We’ll explain those when we go over the technical details of the process.
Track the current hook
Before doing any processing, the plugin API needs to track the current hook that you’re calling. It does so by pushing the current hook name on top of the wp_current_filter
array. This is part of the stack behaviour of wp_current_filter
that we described earlier.
Process “all” hook functions
The next step in the process is to check if there are any functions registered to the “all” hook. This is a special hook. The plugin API will call the functions registered to it for every hook in WordPress. These functions are always called before the regular hook functions.
_wp_call_all_hook
takes care of calling every function registered to the “all” hook. It’s an internal function designed for that purpose. It’s used by every hook processing function in WordPress.
It’s important to note that _wp_call_all_hook
and, by extension, the “all” hook ignore priority. The plugin API doesn’t sort them like it does for regular hooks. So if you have more than one function registered to it, they’ll run in the order the API added them.
It’s also worth noting here that “every hook” does mean every action and every filter. As you saw earlier, the plugin API can’t see the difference between the two. So you can’t have a function that runs for every filter or every action.
This leads us to a small warning. You have to be careful when using the “all” hook. WordPress calls hundreds of hooks on each page load. It’s easy to slow down a site if you aren’t careful when using it.
Sort all registered functions
At this point, the plugin API hasn’t started processing the specific hook you’re calling. Before doing so, it needs to make sure that it sorted the registered functions by priority. We saw that this isn’t done by default when we looked at wp_filter
.
So the plugin API verifies that by looking at the merged_filters
array. If it finds the hook name in it, it means that the registered functions aren’t sorted. It’ll proceed to sort them using ksort
. Otherwise, it does nothing.
Extract the arguments
The plugin API also needs to get all the arguments that you passed to the processing function. It’s an important step because you can pass as many arguments as you want to the function. That’s why you need to an accepted_args
value when registering a hook function.
PHP has a few functions that the plugin API uses to extract these arguments. It uses func_get_args
to get all the arguments passed to the function. Otherwise, it uses a combination of func_num_args
and func_get_arg
to get a subset of arguments.
Call all registered functions
The plugin API is now sure that it sorted all the registered functions for the hook you’re calling. It can now proceed with calling them. It uses a double loop to do it.
It’ll start by looping through each priority in wp_filter
. For each of them, it’ll loop through all the registered function arrays. It calls each of them using call_user_func_array
.
To prevent errors, it’ll check that there’s a value under function
before trying to call it. If there’s a value under function
, it’ll use that value for the callback
parameter of call_user_func_array
. The param_arr
argument is a subset of the arguments extracted earlier. The plugin API builds that subset by using array_slice
with the accepted_args
value.
Remove current hook tracking
There’s one step left once the plugin API finishes calling all the registered functions. It needs to remove the current hook from the top of wp_current_filter
array. This ensures that wp_current_filter
continues to act as a stack data type.
Technical details
Alright! So you got a good overview of what goes on when the plugin API processes hook functions. We’re going to push things a bit further by going into the technical details of the process.
There are four functions that you can use to process hook functions. There’s two for “action hooks” and two for “filter hooks”. You can also classify them into two “flavors”: standard and “ref_array”.
These flavors represent how the plugin API handles arguments. All four functions accept an infinite amount of arguments. They just don’t extract and pass them to registered functions the same way.
Standard functions
First off, you have your standard processing functions. You have apply_filters
for “filter hooks” and do_action
for “action hooks”. Those are the functions you know and love. There are thousands of calls to them throughout WordPress.
function apply_filters($tag, $value) function do_action($tag, $arg = '')
When you look at these two functions, it looks like they accept only two arguments. Both have the tag
parameter to identify the hook name. apply_filters
has the value
parameter. It’s the variable that all registered functions will apply changes to.
Meanwhile, do_action
takes arg
as its second parameter. It lets you pass an extra argument to do_action
. It exists to maintain backwards compatibility with old (more like ancient) versions of WordPress.
That said, you know this isn’t needed. Both these functions accept an unlimited number of arguments. They use PHP functions to extract the arguments from the function call.
do_action
has extra logic to merge arg
into all the the rest of arguments. This lets it maintain its backwards compatibility. It’s also main distinction between the two functions.
“ref_array” functions
You also have the less common “ref_array” functions: do_action_ref_array
and apply_filters_ref_array
. They’re legacy functions used back in PHP4. They’re not as common now and we’ll see why in a moment.
Back in the PHP4 days, there were issues with how PHP handled references. These issues also caused problems with our standard functions. These functions allowed developers to work around the problem.
function apply_filters_ref_array($tag, $args) function do_action_ref_array($tag, $args)
It’s done by limiting both functions to two parameters: tag
and args
. tag
is still the parameter that identifies the hook name. It hasn’t changed from the standard functions.
The args
is an array of all the arguments used by the “ref_array” functions. It replaces the use of PHP functions to extract all the arguments passed to the functions. Instead, all they do is manipulate the args
array.
It’s worth noting that this applies even to the value
parameter from apply_filters
. There’s no value
parameter for apply_filters_ref_array
. It uses the value at index 0 in the args
array as the value
.
Should you use these functions? That’s the real question here. After all, PHP4 was a long time ago. The answer is that you shouldn’t.
There was even an attempt to remove the functions a few years ago. It didn’t end up happening because there were still a few issues that arose at the time. That said, the recommendation was that you should avoid using them.
A note about wp_actions
The last thing to talk about before we move on is wp_actions
. Both do_action
and do_action_ref_array
have code to manage it. It’s the only significant difference between the action and filter hook functions.
if (!isset($wp_actions[$tag])) $wp_actions[$tag] = 1; else ++$wp_actions[$tag];
The code snippet checks to see if there’s a value for the hook in wp_actions
. If there isn’t, it initializes the counter at 1. Otherwise, it increments the existing counter.
Remove hook functions
Here is a scenario that isn’t that uncommon. Let’s say that you’re using a plugin. It does something that you don’t want it to. You need to remove the hooks that it uses to disable it.
The plugin API has a set of functions that can help you with that. remove_filter
lets you remove a specific filter hook function. remove_all_filters
removes all functions registered to a specific filter hook.
There’s also a set of functions for actions hooks: remove_action
and remove_all_actions
. They’re nothing more than wrappers around the respective filter hook functions.
Remove a specific hook function
function remove_action($tag, $function_to_remove, $priority = 10) { return remove_filter($tag, $function_to_remove, $priority); } function remove_filter($tag, $function_to_remove, $priority = 10)
remove_filter
has three parameters: tag
, function_to_remove
and priority
. The goal of the function is to unset a registered function array in wp_filter
. That’s why the function needs the same parameters that it used to create the wp_filter
keys.
The function uses these values to create a unique ID using _wp_filter_build_unique_id
. It then checks to see if there was a registered function array in wp_filter
.
If there was, it removes it. It’ll then do some extra checks to clean up wp_filter
. This process can cause issues if you’re not careful.
Afterward, it removes the flag in merged_filters
. The function also returns the value true
or false
. It’s meant to let you know whether there was a value stored in wp_filter
or not.
Remove all hook functions
function remove_all_actions($tag, $priority = false) { return remove_all_filters($tag, $priority); } function remove_all_filters($tag, $priority = false)
remove_all_filters
is more straightforward. It uses two parameters: tag
and priority
. tag
is the usual hook name.
priority
lets you make a granular choice when removing registered hook functions. The default is to remove them all. If you set a value, it’ll remove only registered functions for that priority
value. It doesn’t do any other checks.
This function also removes the flag in merged_filters
. The return value is always true
though. There’s no way to know if it removed functions or not.
Inspect the plugin API
The last set of functions revolves around the need to inspect the plugin API. You might want to know what hook it’s processing right now. Or if anyone registered functions for a specific hook.
There are a lot of advanced use cases where getting this information is necessary. So let’s take a look at the questions you can ask while the plugin API is running.
What is the current hook?
Let’s start by looking at how you can get the current hook that the plugin API is processing. current_action
and current_filter
are the two functions that you can use to do that. They’re both the same because current_action
is just a wrapper around current_filter
.
function current_action() { return current_filter(); } function current_filter()
The function just returns the last hook added to the wp_current_filter
array. It uses the end
function to do it. It’ll return false
if the plugin API wasn’t processing any hooks.
Is the plugin API processing a hook?
There are some limitations to current_action
and current_filter
. The fact that your hook isn’t the current hook doesn’t mean that the plugin API isn’t processing it. It could still be in wp_current_filter
.
function doing_action($action = null) { return doing_filter($action); } function doing_filter($filter = null)
That’s what doing_action
and doing_filter
checks. You pass it a hook name and it’ll use the in_array
function to see if it’s present. If you pass it a null
value, it’ll just check if wp_current_filter
is empty.
Once again, there’s no difference between doing_action
and doing_filter
.
Does a hook have registered functions?
You might also want to check if a hook has registered functions or not. You can do that using has_action
and has_filter
. Continuing on the existing trend, has_action
is just an alias for has_filter
.
function has_action($tag, $function_to_check = false) { return has_filter($tag, $function_to_check); } function has_filter($tag, $function_to_check = false)
The functions take two parameters: tag
and function_to_check
. tag
is the name of the hook that the function will look at. You can also use function_to_check
to check for a specific registered function.
First, the function checks if there’s anything in wp_filter
at the tag
key location. If it’s not empty, it’ll scan every priority to see if there’s a function registered. It’ll then return whether it found anything or not. By default, that’s true
or false
.
That’s unless you’re using function_to_check
to look for a specific registered function. At that point, it uses _wp_filter_build_unique_id
to build a unique ID for function_to_check
. It’ll then check every priority again for that function using that unique ID. If it does find it, it’ll return the priority it found it at instead of true
.
How many times was an action called?
This last situation is specific to actions only. You might want to know how many times the plugin API processed a specific action hook. You can use the did_action
function to get that information.
function did_action($tag)
It uses the tag
parameter to find the counter stored in wp_actions
array. It’ll return the value of the counter found in the array. Or it’ll return 0
if it coudn’t find a counter.
The cornerstone of WordPress
There’s no understating the importance of the plugin API. WordPress wouldn’t be WordPress without it. It’s that simple.
It’s what gives you full control over WordPress. In other words, it’s the keys to the kingdom (just don’t mess up the paint!). The better you understand it, the better you can control WordPress.
But with great power comes… Nah, just kidding! You go forth and use the plugin API to build (cool) stuff. You know it better than anyone now.