WordPress for the adventurous: Plugin API

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).

wp_current_filter stack

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.

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.

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.

wp_filter ksort priority

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.

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.

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.

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.

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

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

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.

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.

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.

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.

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.

 

  • Dylan Ryan

    Thanks for writing this Carl. I’ve always been curious about what makes the Plugin API tick. I’m also curious if there is a way to return a list of all hooks available (from WP / Plugins / Themes installed). Based on your article, I believe the answer is, it’s not possible because the hooks aren’t visible until they are in the stack. Is that correct?

    • Yeah, I mean there’s an infinite amount of possible hooks in theory. The number of hooks visible can change based on the requests as well.

      You can still inspect what hooks were added/called for each request. Debug plugins like Query Monitor do this.

      There are multiple ways to do it too. You could use the “all” hook. Or you could just inspect $wp_filter near the end of the WordPress request using a shutdown hook.

      It all depends on what you’re trying to accomplish.

  • Great write-up, thanks!

  • Roy

    Great topic, as always #CarlLevel but everyone needs a little Carl sometimes.

  • Another awesome article!
    Thanks Carl!

  • Vlad

    It’s first time I hear about ‘all’ hook. Great article. Will read it again as it’s hard to get all the details from the first read.

    Please use more illustrations, schemes and code examples.

    • Don’t mind adding more of those! Where do you feel they’re needed?

      • Vlad

        I’d added diagram or illustration for every section/part of the article. It’s a long read and people who remember better visually will get idea/remember it quicker. Guys like Neil Patel doing great job mixing long reads with a lot of visuals.

        BTW great idea to drip your subscribers with blog posts. I’d never come to this article without email invitation.