Over and over again, I come across various code snippets that are using WordPress filters, which are part of the Plugin API of WordPress, in some wrong way. This post is me trying to make people aware of the misusage, and also to educate people on the correct usage of WordPress filters.
The Plugin API
WordPress has a built-in set of functions that allow for various things to be done or to be changed during the life cycle of a request in WordPress. These functions are also known as Plugin API.
The most important and also well-known ones are add_action()
, add_filter()
, apply_filters()
and do_action()
, and as you can guess from these names, the Plugin API is concerned with two different things: actions and filters.
WordPress Actions
Every time when something meaningful is about to happen, or when it did just happen, WordPress offers an entrypoint to the life cycle.
In plain words, WordPress is telling us something like this:
Hey! XYZ is about to happen (or it just happened). Do you want to do something now? I can also give you this and that as context.
This means that we can take action whenever we want to, which may be based on the data we get passed along from these individual entrypoints. We cannot change any data here, but we can kick off some process, and also make it use the data we get.
WordPress Filters
In contrast to actions, WordPress filters are all about data. They also provide an entrypoint, but rather than accompanying some specific time in the life cycle, they are bound to usage of data.
The according plain words example would look something like this:
Hey! I have this piece of data here. Do you want to do something to (or maybe just with) it now? I can also give you this and that as context.
This means that we can manipulate any data that is to be (or has been) used by WordPress, or a plugin or the theme.
And What About Hooks?
Now, if you have ever come across actions or filters before, you might be wondering why I didn’t mention hooks so far. Aren’t both actions and filters hooks?
My, pedantic, answer is: No. No, they are not really.
Let’s have a look at some code example:
// Somewhere in WordPress:
do_action( 'save_post', $post->ID, $post, true );
// In some plugin file:
function send_post_to_remote( $id, \WP_Post $post ) {
// Do something with $post.
}
add_action( 'save_post', 'MyNamespace\\send_post_to_remote', 10, 2 );
In the above code, save_post
is the name of the action. It allows us to hook up the send_post_to_remote
callback that is to be run when the desired action occurs. This means we have action callbacks hooked up to action names, fired by WordPress (i.e., the whole do_action
line/statement). And this last bit—the usage of do_action()
—represents or rather creates an action hook.
Of course, similar things are true for filters. In that case, however, the filter is actually the filter callback, and the filter hook itself is the usage of apply_filters()
.
So, to get that right: The concepts of actions and filters are really similar, while the entities action and filter are completely different things. This is an important thing to know.
Best Practices Around WordPress Filters
Now that we clarified some naming and maybe also philosophical things, let’s get to the (mis)usage.
Return Appropriate Data
TL;DR: If a filter callback does not return data in every single (program flow) branch, it is broken!
Filters (i.e., the callbacks) for one and the same filter (hook) name are stacked on top of each other. This means that WordPress takes some piece of data, then passes it to the first filter, taking its result and passes it to the second filter, and keeps on doing this all the way through to the last filter callback.
This means, no matter what you do in your filter callback, you are expected to return data. If not, PHP will yield null
as implicit return value, which is most of the times not what you want, nor what the data contract—which is essentially the PHPDoc—specifies.
The idea of filters is to manipulate data. But sometimes a filter hook is all you get from WordPress, or a plugin or theme. For example, the Menu Cache plugin that I developed has to make use of the wp_nav_menu
filter hook, just because there is no appropriate action hook to use. But even then I have to make sure the next filter gets valid data, so I need to return what I get.
Do Not Redefine Complex Data
TL;DR: If you return data without taking into account all current data, you might be causing (huge) downstream problems!
When you get some complex or non-scalar value passed to your filter callback, do not redefine it. Instead, manipulate it to your liking; if possible.
This comes in various flavors, so let me illustrate two of them.
Manipulate Arrays
Let’s assume we get the following array as value to filter:
[
'limit' => 10,
'orderby' => 'date',
'order' => 'desc',
]
Let’s also assume we wanted to make the consuming function of this $args
array limit the result to just three objects, and also order them by date ascendingly. Now what we could do is to have a filter that looks like so:
function bad_customize_args() {
return [
'limit' => 3,
'orderby' => 'date',
'order' => 'asc',
];
}
See how we not even expect any data (in the function signature)? Sometimes the filter callback specifies one or more parameters, but actually doesn’t use them at all. So this is just the same.
Now, what is the problem with this? The query should be as we want it to be right?
Well, maybe. But we also might get a PHP notice, or even a warning or (fatal) error, depending on what exotic things the consuming code might be doing with the data.
Why is that, you ask? Well, let’s assume the original array gets extended, and now also contains a fourth element. In our filter callback, we both don’t specify it, nor do we even know about it. Even something as simple as a one-dimensional associative array defines a (simple) data structure.
Also, we don’t really do what we want. We want to specify two concrete values for two settings. But what we actually do is define the whole array to only have the elements that we specify.
So, what is the correct way to do this? That’s easy, and could look like so:
function better_customize_args( $args ) {
return array_merge( (array) $args, [
'limit' => 3,
'orderby' => 'date',
'order' => 'asc',
] );
}
We actually take all there is, and then just specify what we want. This may be changing values of existing keys, or specifying new values altogether.
Manipulating Objects
Similar to arrays, we might receive an object in our filter callback. And similar to the array example, we should not set up a new object that conforms to our desires, but rather make the existing one do.
A really bad example, which I actually saw like that multiple times in the wild, is to take a WP_Post
post object, and then return a plain stdClass
that is no more than a post data object.
If WordPress passes a WP_Post
object, and the PHPDoc tells us that the value is a WP_Post
object, well, then there’s no valid reason or excuse to pass anything else. Except for something that fulfills the same contract, which is not possible, because the WP_Post
class is declared final
.
But also, if you want to pass on a post with some specific property value, please don’t create a new post object and make it have that value. Take the current object, and make it have the value you want it to have.
This is almost always possible, but could come in varying disguise. Most of the times, the object comes with a setter that you can use. In WordPress, where people love public properties, you might actually be able to set the value directly on the object. In case of an immutable object, the according class might have a modifier method (e.g., $bar->with_foo( $foo )
) that you can use to create a new object, based on the existing one. This last emphasized bit is important.
Do Not Manipulate the Context
TL;DR: If a filter callback has any sort of side-effect, it is close to be broken!
Everything that you get in addition to the one piece of data to be filtered should be treated as read-only data. Contextual data should only provide you with more information about the data that you are allowed to manipulate, for example, the object that the piece of data belongs to, or where in the program flow the data is being used how.
In case of arrays or scalar values, for example, some string or Boolean, persistent manipulation is not even possible. But due to the nature of PHP and how objects are passed (as pointers), you could manipulate any object for real. Just don’t.
This is also true for actions, where all you get is context. Do not manipulate anything passed to your action callback!
Take Action!
add_filter( 'the_comments_of_this_post', function ( array $comments ) {
$comments[] = new \WP_Comment( /* TODO: Use comment form below... */ );
return $comments;
} );
😀
Leave a Reply