NewCity

Getting Hooked on WordPress Hooks

What are Hooks?

WordPress can be an imposing environment for new developers. The barrier to entry is low — you can accomplish a lot by pasting snippets into your theme’s functions file — but it doesn’t take long to realize that WordPress is an enormous machine full of moving parts. Every page load requires a sequence of thousands of tasks that run in a specific order, and any code you write has to fit into that order. If you get the order wrong, your code might not run at all, or worse, it could bring down the entire site with a “white screen of death.”

WordPress provides developers with “hooks” that allow us to run our code at specific times. You have probably seen code snippets in the WordPress documentation that look like this:

/**
 * Register a custom post type called "product".
 */
function wpdocs_codex_product_init() {
    $args = array(
        // Args for registering the post type
    );
 
    register_post_type( 'product', $args );
}
add_action( 'init', 'wpdocs_codex_product_init' );

This snippet creates a callback function and adds it to a hook called init. Why not run wpdocs_codex_book_init() directly, instead of using add_action()? Because at the time that this block of code runs, WordPress isn’t completely initialized, so register_post_type() doesn’t have access to all of the values and functions it needs to work. The init hook runs after the WordPress environment is loaded but before the current request is processed. By adding your function to the correct hook instead of running it as soon as functions.php loads, you’re delaying the execution of the function until the necessary setup is finished.

JavaScript hooks

WordPress has had PHP hooks for a long time, but more recently it expanded the hooks concept to its JavaScript API. JavaScript hooks are outside the scope of this blog post, but the syntax for the JavaScript functions is very similar to the PHP equivalents. See the WordPress documentation on JavaScript hooks for more information.

Hooks from Plugins

The init hook is one of dozens of action hooks and filter hooks built into the WordPress core. Plugin developers can also add their own hooks to make their plugins extensible by other developers. The popular Advanced Custom Fields (ACF) plugin has more than 30 action and filter hooks that allow third party developers to add features or change existing behavior. This flexibility has allowed for a diverse ecosystem of ACF add-ons, ranging from simple plugins that add one new field type to complex ones that dramatically extend and modify the entire ACF interface.

Another example is the popular calendar plugin “The Events Calendar,” which includes plugin-specific hooks like tribe_template_before_include and tribe_template_after_include, which run just before and just after the template is rendered. These hooks, and others like them, let theme developers customize the appearance and behavior of calendar and event views without risking compatibility problems with future versions of the plugin.

Writing your own hooks

As a WordPress developer, you aren’t limited to using the action and filter hooks that already exist. By placing hooks strategically in your own code, you can allow other developers to work within and around that code without editing it directly. Even if you aren’t opening your code up to other developers, hooks can be a great way to make your code more powerful and reusable for your own purposes.

The following examples demonstrate how you might use custom hooks in your own projects. These examples focus on filter hooks rather than action hooks. Custom action hooks are most useful for code involving multi-step lifecycles. For example, Advanced Custom Fields has actions that run when a field is created, when it is updated, when its value is read, and many more. Most projects are not that complex and will benefit more often from filters that modify a value before passing it on. If you do have a good use case for action hooks in your own code, you can apply most of the same process described here for filter hooks and fill in the remaining details using the WordPress documentation for do_action().

Real world example: Breadcrumb Builder

I write a lot of custom themes at NewCity. Many of those themes have features that are similar from project to project but aren’t exactly the same. I might be able to copy and paste some of my code from previous themes, but I’ll always need to modify some behavior to fit the specific project I’m working on. By using filter hooks, I can write my functions in a modular way that lets me separate the code that doesn’t change from the code that does change. For example, many of the sites I build need breadcrumb navigation on hierarchical pages. Every function that builds a list of breadcrumbs needs to perform the same steps:

  1. Get a list of the current page’s ancestors
  2. Create an array of link values for the ancestor pages
  3. Put the array in order with the highest level (closest to the site root) pages first

The resulting breadcrumbs list, when processed and turned into markup, would look something like this:

Grandparent Page > Parent Page

Building breadcrumbs without hooks

For my first theme with breadcrumbs, I wrote a function like this, which performed all of the steps above and returned an array:

function nc_get_breadcrumbs( $post_id = null ) : array {
    $post_id = $post_id ?? get_the_ID();
    if ( !$post_id ) {
        return false;
    }    if ( !has_post_parent( $post_id ) ) {
        return false;
    }    $ancestors = get_post_ancestors( $post_id );
    if ( !$ancestors ) {
        return false;
    }    $ancestors = array_reverse( $ancestors );
    $breadcrumbs = array_map( function ( $ancestor_post_id ) {
        return [
            'title' => get_the_title( $ancestor_post_id ),
            'url' => get_permalink( $ancestor_post_id ),
        ];
    }, $ancestors);
    return $breadcrumbs;
}

Home > Grandparent Page > Parent Page
Most breadcrumb navigation needs more than just the core behavior, though. For my next theme, I needed to add a link to the home page to the start of the breadcrumbs.

Because I wanted to be able to reuse the function on future projects, I added an argument to the function to enable or disable this new behavior as needed:

function nc_get_breadcrumbs( $post_id = null, bool $include_home = false ) : array {
    // All of the code from the previous version...
    if ( $include_home ) {
        array_unshift( $breadcrumbs, [
            'title' => __( 'Home', 'nc-base-theme' ),
            'url' => get_home_url(),
        ] );
    }
    return $breadcrumbs;
}

I started my next theme thinking I could reuse my existing breadcrumbs function without modification. Then I learned that for this theme, I also needed to include the current page at the end of the list.

Home > Grandparent Page > Parent Page > Current Page

I could have added yet another argument to the function…

function nc_get_breadcrumbs( $post_id = null, bool $include_home = false, bool $include_current = false ) : array {
    // This is starting to get unwieldy...
    if ( $include_current ) {
        $breadcrumbs[] = [
            'title' => get_the_title( $post_id ),
            'url' => get_permalink( $post_id ),
        ];
    }
    return $breadcrumbs;
}

But a troubling pattern was emerging: I was three themes in, and I still couldn’t use the same breadcrumbs function on two projects in a row. What if I needed to make yet another change next time? There would be no way to predict every possible feature I would ever need.

Building breadcrumbs with hooks

The solution was to refactor the function, removing the project-specific code from the main function and applying it using filter hooks instead. I went back to the original version of the function, removing the homepage and current page behaviors, and added a call to apply_filters():

function nc_get_breadcrumbs( $post_id = null ) : array {
    $post_id = $post_id ?? get_the_ID();
    if ( !$post_id ) {
        return false;
    }    if ( !has_post_parent( $post_id ) ) {
        return false;
    }    $ancestors = get_post_ancestors( $post_id );
    if ( !$ancestors ) {
        return false;
    }    $ancestors = array_reverse( $ancestors );
    $breadcrumbs = array_map( function ( $id ) {
        return [
            'title' => get_the_title( $id ),
            'url' => get_permalink( $id ),
        ];
    }, $ancestors);
    // Process the default breadcrumbs list with filters
    // assigned to 'nc_breadcrumb_links'
    $breadcrumbs = apply_filters( 'nc_breadcrumb_links', $breadcrumbs, $post_id );
    return $breadcrumbs;
}

This instance of apply_filters looks for filter functions registered with the nc_breadcrumb_links hook. It then passes the initial value of $breadcrumbs to the first function, passing the output of each callback to the next callback in the sequence. I have also chosen to include the value of $post_id as an additional argument. The $post_id value will not be modified, but it will be available to each callback in the hook’s sequence.

Until I register some callbacks with nc_breadcrumb_links, this call to apply_filters will return the initial value of $breadcrumbs without modifying it. To add the home page link to the beginning of the list, this time with a hook, I can add the following filter function to page.php:

function nc_breadcrumbs_add_home( array $breadcrumbs ) : array {
    array_unshift( $breadcrumbs, [
            'title' => __( 'Home', 'nc-base-theme' ),
            'url' => get_home_url(),
        ] );
    return $breadcrumbs;
}
add_filter( 'nc_breadcrumb_links', 'nc_breadcrumbs_add_home', 10, 1 );

To add the current page link to the end of the breadcrumbs, I can add another filter function. You could combine both features into the same filter function, but I am separating them to give myself the option of using only one or the other in different situations.

function nc_breadcrumbs_add_current( array $breadcrumbs, $post_id ) : array {
    $breadcrumbs[] = [
        'title' => get_the_title( $post_id ),
        'url' => get_permalink( $post_id ),
    ];
    return $breadcrumbs;
}
add_filter( 'nc_breadcrumb_links', 'nc_breadcrumbs_add_current', 10, 2 );

The nc_breadcrumbs_add_home and nc_breadcrumbs_add_current() callbacks differ in one important way: the only argument that nc_breadcrumbs_add_home() needs is the breadcrumb array, but nc_breadcrumbs_add_current() needs both the array and the $post_id value. When adding the two filters to the nc_breadcrumb_links hook, I set the final $accepted_args argument of add_filter() to 1 for the homepage link filter and 2 for the current page filter. If either of those values did not match the number of required arguments in their respective functions, WordPress would throw a PHP error.

Because one of these callbacks changes the beginning of the array and the other changes the end of the array, the order that the filters are run does not matter. I gave both of them a priority of 10, which is the default priority for new WordPress hooks. What if we added a third callback, this time adding a parent site link to the start of the array?

Parent Site > Home > Grandparent Page > Parent Page > Current Page

function nc_breadcrumbs_add_parent_site( array $breadcrumbs ) : array {
    array_unshift( $breadcrumbs, [
            'title' => __( 'Parent Site', 'nc-base-theme' ),
            'url' => get_home_url('https://insidenewcity.com'),
        ] );
    return $breadcrumbs;
}
add_filter( 'nc_breadcrumb_links', 'nc_breadcrumbs_add_parent_site', 20, 1 );

If this callback ran before nc_breadcrumbs_add_home(), the two added links would be in the wrong order, with the home page link coming before the parent site link. Fortunately, the third argument of the add_filter() function is a “priority” value. Filters added with a lower number will be applied earlier, so we can assign nc_breadcrumbs_add_parent_site() a priority of 20 to make sure it runs after nc_breadcrumbs_add_home(), which has a priority of 10.

Are you hooked?

Skilled use of WordPress hooks is important for any WordPress developer. Even if you don’t think you will be creating any custom hooks for real projects, I highly recommend building a few practice examples. Working through the hooks lifecycle from beginning to end will give you a deeper understanding of how they operate, which will level-up your skills at using core and third-party hooks as you encounter them.

NewCity logo