WTF is a Hook?!

WTF is a Hook?!

WordPress hooks – The bane of every newbie WP developer.

My goal with this post is to outline WordPress’ action and filter hooks in a plain-English way that I wish someone had done when I was first wrapping my head around WordPress. My hope is that you leave here with a greater understanding of how hooks work, when you want to use them, and why. Gone will be your days of blindly copying a block of code from StackOverflow into functions.php and crossing your fingers. 🙂

WP Hooks Basics

Let’s start by answering the question in the title of this post. What even is a WordPress hook?

Boiled down to the most basic explanation: A hook is a way for you as a custom Theme or Plugin developer to override, inject, or otherwise take control over some aspect of WordPress core, the internals of a Plugin, or parts of a Theme. Most of the time the hooks will pass along some information that is relevant to the action being performed in or around the hook.

The most common use of hooks is to customize something that you don’t have direct access to (e.g. WordPress core, or a Plugin from the WP Repo). That being said, you can also create your own hooks which I will touch on in more detail below.

If you are a custom Plugin developer, creating your own hooks is an essential way to make sure that Theme developers are able to customize the functionality of your Plugin, since the best practice is to not modify Plugin code directly. If you are building your own Theme for private use, you will inevitably need to use a hook from some of the Plugins that you choose to use in order to build custom functionality. You can also create your own hooks in the context of your theme, though this is less common since you will have direct access to your own theme code and much of what you’d do with a hook could be done with template partials and custom functions.

The add_action and add_filter functions that allow you to interact with hooks allow you to specify the total number of parameters you are expecting to get from the hook, and the priority that you want to run your code at. You aren’t required to use any of the parameters if you don’t need them, but you do need to specify them in your function definition if you supply a number greater than 0 in add_action or add_filter.

Difference Between Action Hooks and Filter Hooks

There are two types of hooks: Action Hooks and Filter Hooks

Action hooks are generally used in the context of injecting some code into a specific area. You’ll often see action hooks before and after key components in a Plugin’s template. WP Core has a huge amount of action hooks that are fired at different times during the page load process. These action hooks allow you to execute your own custom code at specific intervals. Action hooks sometimes pass along parameters relevant to the surrounding code, but not always. Usually, action hooks don’t return any value.

Filter hooks are generally used to intercept and/or modify some specific output. Much like action hooks, you’ll see filter hooks implemented by Plugins and WP Core. The point of filter hooks is to allow developers to modify a particular value before it is used and/or rendered. A filter hook always has at least one parameter — the value passed to the developer — and can have additional contextual parameters. Filter hooks should generally return a value.

Anatomy of an Action Hook

Action hooks are generally used to indicate when something is about to happen, or has happened, and allow you as an external developer to inject some additional functionality at that point. An action hook can pass 0 or more parameters containing useful context, and the hook functions will generally not return any value.

Let’s start with an example of using somebody else’s action hook in your custom theme.

Using Action Hooks

I’m going to show an example of hooking WordPress core’s save_post_post action. This action fires any time a Post is created or updated, and we get 3 parameters with it: $post_id, $post, and $update. According to the documentation page, $post_id is the ID of the saved Post, $post is the WP_Post object, and $update is a true/false value indicating whether this was a creation action or an edit action.

Let’s say that you want to programmatically set a custom field to track the current time every time a new Post is created. To do that we would add the following to functions.php:

function mytheme_send_email_on_post_creation( $post_id, $post, $update ) {

    // If this is an existing Post, do nothing
    if( $update ) {
        return;
    }

    // Sets my_custom_hook_flag to the current time (using php tiemzone) when a Post is successfully created
    update_post_meta( $post_id, 'my_custom_hook_flag', date( 'H:i:s' ) );
    
}
add_action( 
    // The name of the action hook
    'save_post_post', 

    // The name of our hook function
    // This can be anything but should reflect what your function does. The "mytheme_" prefix 
    // can be changed to your own desired prefix
    'mytheme_send_email_on_post_creation', 

    // Run our action extremely late
    // In our case we want this to run after any other potential hook functions
    9999,

    // Sets the number of expected params to 3
    3
);

Notice that we used the add_action function to hook into the action. This function requires at least the name of the action hook and the name of the function that you want to run at that hook. It optionally allows us to specify a priority and argument count.

The priority defaults to 10 if not specified, and it basically tells WordPress where to run your function in the queue of all functions interacting with the specified action hook. So, if you need to make sure that your function runs before others using the same hook, you’d want to set the value to 0, or at least lower than 10. Conversely, if you need to make sure your function doesn’t run until after any others have, then you’ll want to set priority to a very large value. Usually priority comes into play if you are dealing with multiple plugins and/or theme functions interacting with the same piece of functionality.

The params value defaults to 2 if not specified, and it simply tells WordPress how many parameters it should pass into your function. You can set this number less than or equal to the number of parameters passed into the do_action call that registers the hook. That will determine how many of those parameters you can use in your custom function.

Creating Action Hooks

Next, let’s look at what it might look like to create our own action hook for somebody else to use. Let’s say I have a function do_some_magic() inside of a custom Plugin that I’m building.

function do_some_magic( $post ) {   
    echo wp_kses_post( $post->post_content );
}

Now, let’s say I want other developers to be able to detect when that function is running and run their own code. I want them to be able to hook the function both before and after my echo statement, so I will add action hooks in both places:

function do_some_magic( $post ) {   

   do_action( 'my_plugin_before_do_some_magic' ); 
   echo wp_kses_post( $post->post_content );
   do_action( 'my_plugin_after_do_some_magic' );

}

Notice that I prefixed my hook with “my_plugin”. Using unique prefixes when defining new hooks is best practice in order to avoid naming collisions with other themes, plugins, or WordPress core.

Okay great, now we have a function with 2 action hooks available for developers to use. We can leave it like this, but notice that a $post parameter gets passed to my function. It might be nice for other developers to have that for some context, perhaps they only want to do something for a specific post, for example. Let’s add that to our action hook declarations:

function do_some_magic( $post ) {   

   do_action( 'my_plugin_before_do_some_magic', $post ); 
   echo wp_kses_post( $post->post_content );
   do_action( 'my_plugin_after_do_some_magic', $post );

}

Finally, using the methods from the section above, I can test out my new hook by implementing a hook function in my theme’s functions.php:

function mytheme_before_do_some_magic( $post ) {   
    if( $post->ID != 1 ) {
        return; // exit if not post ID 1
    }

    echo 'This is Post ID 1!';
}
// We omit priority and arg count because the default priority (10) and arg count (1) 
// work perfectly for us in this case 
add_action( 'my_plugin_before_do_some_magic', 'mytheme_before_do_some_magic' ); 

Anatomy of a Filter Hook

Filter hooks are generally used to override some value to be used or returned somewhere.

Using Filter Hooks

Using filter hooks is very similar to using action hooks. The main differences are that we will use the add_filter function, rather than add_action, and we will expect a value to be returned to us from the filter.

I’m going to show you a simple example of hooking WordPress’ excerpt_more filter in order to change the default […] text that appears after the excerpt.

/* ... functions.php ... */
function mytheme_excerpt_more( $more ) {

    $url = get_permalink( get_the_ID() );
    $more = sprintf( '<a href="%s" class="more-link">Read On...</a>', $url );

    return $more;

}
add_filter( 'excerpt_more', 'mytheme_excerpt_more' );

This function replaces the […] text with a link to the Post that reads “Read On…”.

You may have noticed that we are passing in a $more parameter here, but we aren’t actually using it. Similar to how action hooks work, filter hooks can be passed contextual information. The main difference being that the first parameter passed to a filter hook will always be the value that was expected to be returned. So, in this case, $more will contain the original excerpt more text before we do anything to change it.

We can see this in action by adding a safety check to our custom excerpt function. The modified function below will check if we are on an admin page, and if so it will return the original, unmodified version of the excerpt more text. Otherwise, we will return our custom excerpt more text.

/* ... functions.php ... */
function mytheme_excerpt_more( $more ) {

    if ( is_admin() ) {
	    return $more;
    }

    $url = get_permalink( get_the_ID() );
    $new_more = sprintf( '<a href="%s" class="more-link">Read On...</a>', $url );

    return $new_more;

}
add_filter( 'excerpt_more', 'mytheme_excerpt_more' );

Sometimes you will see multiple parameters passed into a filter hook. The first parameter will always be the expected return result, each additional parameter is simply for context.

There is another way you’ll sometimes see filter hooks used, which is to pass a value through an existing filter hook. The reason that this works is because, by passing your value through that filter, you are inheriting all of the functionality that has already implemented this filter hook.

In the simple example below, I’ve got a custom field that contains some arbitrary value. For the sake of the example assume it contains input from the WordPress WYSIWYG editor. I want to output that value in my page template, as if I were calling the_content().

/* ... page.php ... */
global $post;
$my_custom_content = get_post_meta( $post->ID, 'my_custom_content', true );

// Apply the the_content filter to our custom content to ensure it 
// gets the same treatment as the standard post content
echo apply_filters( 'the_content', $my_custom_content );

Creating Filter Hooks

Similar to how we can create our own custom action hooks, WordPress allows us to create our own filter hooks too. The concept is pretty simple, and looks a lot like passing your value through an existing filter.

Let’s say we are writing a custom plugin and, in our plugin, we have a value that we want to allow other developers to control. For the sake of example, I’m going to say let’s imagine that we have a set of custom query args and we want to expose the posts_per_page value.

/* ... my-custom-plugin.php ... */
function myplugin_query() {

    ...
    $args = array(
        'post_status' => 'publish',
        'post_type' => 'post',
        'posts_per_page' => 25,
    );
    ...

}

We can create a custom filter that allows developers to hook into our plugin and override the value 25, which is currently hard-coded into our query.

/* ... my-custom-plugin.php ... */
function myplugin_query() {

    ...
    $args = array(
        'post_status' => 'publish',
        'post_type' => 'post',
        'posts_per_page' => apply_filters( 'myplugin_query_posts_per_page', 25 ),
    );
    ...

}

Now we’ve created a filter myplugin_query_posts_per_page that can be used by any theme developer in functions.php, or any plugin developer in their plugin to override our value. The name myplugin_query_posts_per_page is arbitrary and can be any name that you think makes sense, provided it does not collide with any other filter or action hook names. Prefixes are considered good practice to prevent hook name collisions.

The only remaining thing to do is to optionally add some documentation. This is not required, but it is considered good practice in order for others reading your code to understand what your hooks do.

/* ... my-custom-plugin.php ... */
function myplugin_query() {

    ...
    /** Filter Hook: myplugin_query_posts_per_page 
     *
     * Allows overriding of posts_per_page in myplugin_query
     * @param int $posts_per_page
     * @return int $posts_per_page
    **/
    $args = array(
        'post_status' => 'publish',
        'post_type' => 'post',
        'posts_per_page' => apply_filters( 'myplugin_query_posts_per_page', 25 ),
    );
    ...

}

How to Identify Available Hooks

The first place to look for information about available hooks is always the documentation. WordPress core has most of its hooks documented fairly well. You’ll also find that most Plugins will at least have a list of available hooks and brief descriptions in their documentation.

If you find the documentation lacking, the next best step is to read the code. Depending on the size of the Theme/Plugin that you are hoping to extend it can sometimes be a challenge to find what you are looking for, but the idea is to find the functionality that you want to hook into, and read the surrounding code to see if the developer has exposed any useful hooks in that area that you can use to accomplish your goal.

Comments

Leave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *