Hooking WordPress Taxonomy Changes With The Plugins API

Captain Hook twirls his moustache at Breakfast in the Park with Minnie & Friends at the Plaza Inn by Loren Javier, on Flickr

So you’ve developed a plugin, or a theme function, in WordPress that needs to know when a taxonomy term — in a category, tag or custom taxonomy — is added, edited or deleted.

Unfortunately, the WordPress Codex barely mentions how to do this, and the examples you see out there on the Web basically regurgitate the same odd instructions, which boil down to passing in some integer parameters that can’t possibly be correct for every example.

Like this:

add_action('create_stores', 'my_create_stores', 10, 1);

That’s unfortunate. So let’s clear up the mystery of how WordPress notifies plugins and theme functions of a taxonomy change, and how we can go about writing a sensible treatment for those events.

Quick-And-Dirty Tutorial: Action Hooks

In WordPress, an action hook is basically a way of intercepting — “hooking” — some step in the process that WordPress uses in going about as task, such as rendering a web page.

Here’s a basic diagram of how WordPress goes about rendering a post:

This is often called the “lifecycle” of a web page, as it represents everything that happens on the web server from the moment the page is requested, or “born,” to when the server is done sending it to the web browser, or “dies.”

Of course, here in the age of the web app, a page lives long after it’s been rendered. But to an old-school, request-response model httpd daemon, a page is dead once it leaves the nest.

A stripped-down version of the WordPress page lifecycle. Several possible events have been removed for simplicity's sake. Source:  WordPress Plugin API Action Reference page.
A stripped-down version of the WordPress page lifecycle. Several possible events have been removed for simplicity’s sake. Source: WordPress Plugin API Action Reference page.

We can intercept any of these steps and modify what WordPress does at that point — either change something it would normally do, add something for it to do, or make it not do something it would normally do.

That is, we can “hook” that “action.” Thus, “action hooks.”

For example, if we add a custom taxonomy in our functions.php file or a plugin, WordPress sees that happening during the first step of the page lifecycle: the “register taxonomies and post types” step. WordPress then does what it needs to do to implement our custom taxonomy, and proceeds to the next step of the lifecycle, “initialize theme and load plugin code.”

Another example is saving a post. Every time that happens, WordPress creates the action save_post.

As the examples at the Codex show, by hooking the save_post action, we can do all kinds of things, such as sending an email to people telling them about the post. Or maybe our cache plugin uses the save_post function to rebuild our site cache. Or maybe our theme’s function.php uses save_post to automatically makes new posts sticky.

WordPress provides both action hooks and filter hooks.

The difference between them is, an action hook tends to focus on doing things that aren’t directly related to showing content, while filter hooks tend to work with altering content display.

For example, if you were looking to change how a category page is displayed, you’d probably use a filter hook, since most of what’s happening when we render a category page happens inside The Loop. But if you wanted to know whether a new category was added to WordPress, you’d use an action hook, since that’s not so much about displaying content, as it is about the basic structure of the site’s content.

Often, you could use either an action hook or a filter hook to accomplish the same goal.

Again, the basic guideline is, if you’re looking to modify how or what content is rendered, you use a filter hook; for everything else, you use an action hook.

Quick-And-Dirty Tutorial: Variable Hooks

As I noted in my overview of how taxonomy works in WordPress, WordPress version 2.3 changed how taxonomy works.

Previously, it was structured around the concept of categories, which were basically “sections” of a website, and tags, which related posts to one another.

Version 2.3 broke out of that mold. It effectively allowed us to make taxonomy for several purposes, be it changing how a post is displayed — the “post formats” introduced in version 3.1 — or to create very complex relationships between many different content types.

As a result of that change, WordPress changed the functions used to hook taxonomy changes. Instead of create_category, edit_category, delete_category, create_tag, edit_tag and delete_tag, we now have the new functions create_${taxonomy}, edit_${taxonomy} and delete_${taxonomy}.

The ${taxonomy} part of those tags are replaced by the slug of the taxonomy to which the term is being added, edited or deleted; that is, they are “variable” hooks. And what that means is, we substitute ${taxonomy} with the slug of the taxonomy we want to observe.

So, if we want to watch for changes to the built-in category taxonomy in WordPress, we’d hook the action edit_category. If we wanted to watch for changes to the built-in post_tag taxonomy, we’d hook the action edit_post_tag.

If we want to know when a term is created in a custom taxonomy slugged “bananas,” we’d hook create_bananas.

And if we wanted to catch the deletion of a term in a taxonomy slugged “this-is-cool,” we’d hook delete_this-is-cool.

In other words, in order to hook the creation, updating or deletion of a taxonomy term, we have to know the slug of the taxonomy to which that term belongs. Then, we just prepend create_, edit_ or delete_ to that taxonomy slug to get the correct action hook for that change.

Disambiguation: A taxonomy is a collection of terms; a term is an option within a taxonomy.

For example, in the default install of WordPress, “category” is a taxonomy; “uncategorized” is a term.

So, if you later set up your blog to have the categories Food, Stories, Travelogue and Miscellaneous, then Food, Stories, Travelogue and Miscellaneous are terms; they belong to the taxonomy called category.

How WordPress Calls Taxonomy Action Hooks

Now to the main point: How does one hook a change to a term in a WordPress taxonomy?

Let’s look quickly at the WordPress 3.9 core code, specifically the file at /wp-includes/taxonomy.php. Somewhere around line 2569, we see this:

do_action( "create_$taxonomy", $term_id, $tt_id );

To explain what this is doing, we need some context.

For the examples here, I am going to focus on create_${taxonomy}.

However, it’s worth noting that wherever I use create_${taxonomy}, I could just have easily have used edit_${taxonomy} or delete_${taxonomy}. I’m trying to keep things as simple as possible, to avoid confusion.

Let’s suppose you run a WordPress site that reviews restaurants. Within your website is a custom taxonomy slugged “restaurant_features.” The terms in this taxonomy are the features of that restaurant, such as “open late,” “bar / lounge,” and “valet parking.”

Now, suppose you decide to add the term “cash only” to the restaurant_features taxonomy.

When you do so, the code at line 2569, above, will automatically make an action called create_restaurant_features.

It does that because it knows the slug of the taxonomy to which we added the term is restaurant_features. And it knows the term was created under that taxonomy.

So, because taxonomy changes use a variable hook, described above, WordPress automatically creates the action create_restaurant_features, anytime a new term is put into the taxonomy slugged restaurant_features. And we can, in turn, hook that action.

That code will also send along, to your user-created handler function (more on that, shortly), both the internal WordPress ID of the term “cash only” ($term_id) and the taxonomy ID for for the restaurant_features taxonomy ($tt_id), to which “cash only” now belongs.

Let’s say that another way.

Suppose that in your WordPress database, the taxonomy ID for the taxonomy slugged restaurant_features is 123.

Suppose that once WordPress successfully creates the term “cash only,” that term has the term ID 987.

The moment WordPress successfully creates the term “cash only,” it will effectively call this function:

do_action( "create_restaurant_features", 987, 123 );

By doing that, WordPress looks for any user-created functions that hook the automatically created create_restaurant_features action, and passes to those user functions the ID for “cash only” (987), as well as the ID for restaurant_features (123).

Using add_action To Hook WordPress Actions

So, our term has been created; WordPress knows the ID number of the “cash only” term, as well as the ID of the restaurant_features taxonomy; and it’s looking for user functions to invoke as a result of the term being created.

We therefore need to add an action listener to our functions.php or plugin code. Appropriately enough, we do that with the function add_action.

The first argument of add_action is the action we want to hook. In our example, that action is create_restaurant_features; our taxonomy is restaurant_features, and a term was created under our taxonomy, so WordPress automatically created the action create_restaurant_features when we added the term “cash only.”

The second argument is the handler function we want to call. That’s another function we will write that does all the work.

And we have two, optional arguments we can also pass to that handler function: $term_id, or the ID of the taxonomy term affected; and $tt_id, or the ID of the taxonomy to which that term belongs.

Back to our example.

Let’s suppose that every time we add a new term to the restaurant_features taxonomy, we want to email our salesman, Clem Clement, and let him know that there’s a new term he can sell to advertisers.

First, we create the function that will send that email to Clem.

function send_clem_an_email($term_id, $tt_id) {
	//get the term from WordPress
	$term = get_term($term_id, $tt_id);
	if(!is_wp_error($term)) {
		//we have the term; get its name
		$term_name = $term->name;
		//build the body of the email we'll send
		$body = "Hey Clem, I just created the term $term_name as a restaurant feature. Hit me with the horns and make that money.";
		wp_mail("clem@example.com", "New restaurant feature added to website", $body);

Note that this function is taking the arguments $term_id and $tt_id.

We’re going to get those directly from WordPress when it calls the action.

Remember: When WordPress dynamically creates the action create_restaurant_feature, it also passes along the values for $term_id (the ID of “cash only”) and $tt_id (the ID of the taxonomy restaurant_feature).

This becomes obvious when we create the add_action function that will call send_clem_an_email:

add_action('create_restaurant_feature', 'send_clem_an_email', $term_id, $tt_id);

Where do $term_id and $tt_id come from? They come from the magical line 2569 of wp-includes/taxonomy.php:

do_action( "create_$taxonomy", $term_id, $tt_id );

Again, using our example, this line of code in the WordPress core not only spins up the action create_restaurant_feature, it also passes along the term ID for “cash only”, and the taxonomy ID for restaurant_feature, to our add_action function.

So long as we maintain the names of the arguments passed back to us by magical line 2569, we can use those arguments in our handler function.

Stated another way: Because we were passed the variables $term_id and $tt_id by WordPress, we can ask for the variables $term_id as $tt_id in add_action, and WordPress knows to fill them in with the right values.

So, altogether, our functions.php or plugin code would look like this:

function send_clem_an_email($term_id, $tt_id) {
	//get the term from WordPress
	$term = get_term($term_id, $tt_id);
	if(!is_wp_error($term)) {
		//we have the term; get its name
		$term_name = $term->name;
		//build the body of the email we'll send
		$body = "Hey Clem, I just created the term $term_name as a restaurant feature. Hit me with the horns and make that money.";
		wp_mail("clem@example.com", "New restaurant feature added to website", $body);

add_action('create_restaurant_feature', 'send_clem_an_email', $term_id, $tt_id);
Totally irrelevant aside: If you’re not familiar with the phrase “hit me with the horns and make that money,” you can thank me now for this:

Remember: $term_id and $tt_id Are Optional

What if your handler function just needs to know that a term was created, edited or deleted, but doesn’t need to know anything about the term that was affected?

For example, when I created the WordPress plugin that powers The Esplanade Association’s interactive map, I provided a flat file, containing JSON, which was a representation of a custom taxonomy that contains every park amenity type (e.g., restrooms, docks, telephones, etc.)

Every time a new amenity was added to that list, I needed to rebuild the entire file, in order to get the JSON properly encoded. So the details of the new term didn’t matter; I had to get all the terms in that park amenities custom taxonomy and put them all back out into that flat file.

In that case, I could safely omit $term_id and $tt_id from both my add_action and my handler function.

Back to this post’s example.

Suppose every time a restaurant_features term is deleted, you want to rebuild your sitemap, to avoid having spiders hit a lot of 404s.

Your user function would look something like this:

function rebuild_sitemap_on_term_deletion() {
	//code to rebuild sitemap goes here

And your add_action function would look like this:

add_action('delete_restaurant_feature', 'rebuild_sitemap_on_term_deletion');

Together they would be:

function rebuild_sitemap_on_term_deletion() {
	//code to rebuild sitemap goes here

add_action('delete_restaurant_feature', 'rebuild_sitemap_on_term_deletion');

Again, this would be true of create_restaurant_feature and delete_restaurant_feature.

So, if we wanted to rebuild our sitemap any time a term was added, edited or deleted from restaurant_feature, we would do this:

function rebuild_sitemap_on_term_deletion() {
	//code to rebuild sitemap goes here

add_action('create_restaurant_feature', 'rebuild_sitemap_on_term_deletion');
add_action('edit_restaurant_feature', 'rebuild_sitemap_on_term_deletion');
add_action('delete_restaurant_feature', 'rebuild_sitemap_on_term_deletion');

All links in this post on delicious: https://delicious.com/dougvdotcom/hooking-wordpress-taxonomy-changes-with-the-plugins-api


    1. @Charles: A wild guess would be that you have a function name collision with another plugin. In other words, you are running another plugin that uses the same function name you wrote in your hook; or you named your function to be the same as the name of a function that exists in the WordPress core. Alternatively, it’s possible that your function is taking a long time to perform some task, and other tasks are being interrupted by your code; or, it’s possible your code properly compiles, but throws an exception during its execution. Check your server error logs.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  • Check out the Commenting Guidelines before commenting, please!
  • Want to share code? Please put it into a GitHub Gist, CodePen or pastebin and link to that in your comment.
  • Just have a line or two of markup? Wrap them in an appropriate SyntaxHighlighter Evolved shortcode for your programming language, please!