Justin Tadlock

Custom user taxonomies in WordPress

If you’re at all familiar with taxonomies in WordPress, you already know how awesome it is to add custom taxonomies to your posts or custom post types. WordPress developers have known this for a while.

What many people don’t know is that the current taxonomy schema was added way back in WordPress 2.3. Yes, that means the ability to create and use custom taxonomies has been around since 2007. We didn’t get all the cool functions added until 2.8 though.

However, one thing we’ve had since 2.3 was the ability to create taxonomies for any object type, not just posts. In WordPress, there are several object types:

  • Posts
  • Users
  • Comments
  • Links

So, you can technically create a taxonomy for any object type. Most of WordPress core support is for posts, but the API is extremely well thought out and can handle the other object types with minimal code effort.

This tutorial will focus on registering and using a taxonomy on the user object type. It will not be a 100% solution for everything you can do with a custom user taxonomy. Consider this an extremely rough draft of what’s possible.

Because this is such an in-depth topic, I cannot explain every minute detail. That would make for about a 50-page tutorial. You’ll need to be familiar with a few key areas in WordPress development before proceeding: plugins, themes, users, and taxonomies.

Registering a user taxonomy

In this tutorial, you will use and register a taxonomy called “Profession.” This is just an example, so feel free to experiment and create your own taxonomies for your uses.

To register the user taxonomy, you use the register_taxonomy() function just like you would with any other taxonomy. The following code block will register the profession taxonomy and set up its arguments.

/**
 * Registers the 'profession' taxonomy for users.  This is a taxonomy for the 'user' object type rather than a
 * post being the object type.
 */
function my_register_user_taxonomy() {

     register_taxonomy(
        'profession',
        'user',
        array(
            'public' => true,
            'labels' => array(
                'name' => __( 'Professions' ),
                'singular_name' => __( 'Profession' ),
                'menu_name' => __( 'Professions' ),
                'search_items' => __( 'Search Professions' ),
                'popular_items' => __( 'Popular Professions' ),
                'all_items' => __( 'All Professions' ),
                'edit_item' => __( 'Edit Profession' ),
                'update_item' => __( 'Update Profession' ),
                'add_new_item' => __( 'Add New Profession' ),
                'new_item_name' => __( 'New Profession Name' ),
                'separate_items_with_commas' => __( 'Separate professions with commas' ),
                'add_or_remove_items' => __( 'Add or remove professions' ),
                'choose_from_most_used' => __( 'Choose from the most popular professions' ),
            ),
            'rewrite' => array(
                'with_front' => true,
                'slug' => 'author/profession' // Use 'author' (default WP user slug).
            ),
            'capabilities' => array(
                'manage_terms' => 'edit_users', // Using 'edit_users' cap to keep this simple.
                'edit_terms'   => 'edit_users',
                'delete_terms' => 'edit_users',
                'assign_terms' => 'read',
            ),
            'update_count_callback' => 'my_update_profession_count' // Use a custom function to update the count.
        )
    );
}

I won’t cover all the arguments used in the code above. I’ve written about those in a previous tutorial. However, there are a few arguments you should pay careful attention to:

  • rewrite[‘slug’]: I used author/profession so that the slug would fall in line with the WordPress user slug, author. We’ll need a slight fix for this too, which I’ll get to later in the tutorial.
  • capabilities: For simplicity, I’ve set most of the capabilities to edit_users. Only administrators can manage, edit, and delete professions with this by default. The assign_terms capability is set to read so all users can edit this on their profile page. You’ll want to set up some custom capabilities or configure this to do what you want.
  • update_count_callback: You must use a custom function for this because WordPress expects the taxonomy to be for the post object type.

Once you’ve registered your taxonomy, WordPress really doesn’t do much for you. It won’t add any custom admin pages, meta boxes, or anything of the sort like it does for posts. That’ll be left up to you.

Custom term update count callback

Right off the bat, you’ll already need some custom code. Since WordPress will only update the term counts for taxonomies on posts, you’ll need a function to do this for users.

When you registered your taxonomy, you set the update_count_callback argument to my_update_profession_count. The following code is that callback function.

/**
 * Function for updating the 'profession' taxonomy count.  What this does is update the count of a specific term
 * by the number of users that have been given the term.  We're not doing any checks for users specifically here.
 * We're just updating the count with no specifics for simplicity.
 *
 * See the _update_post_term_count() function in WordPress for more info.
 *
 * @param array $terms List of Term taxonomy IDs
 * @param object $taxonomy Current taxonomy object of terms
 */
function my_update_profession_count( $terms, $taxonomy ) {
    global $wpdb;

    foreach ( (array) $terms as $term ) {

        $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $term ) );

        do_action( 'edit_term_taxonomy', $term, $taxonomy );
        $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) );
        do_action( 'edited_term_taxonomy', $term, $taxonomy );
    }
}

I just kept this extremely simple and modified the _update_post_term_count() WordPress function. Feel free to further modify it to suit your needs.

Creating the manage terms page

Manage user taxonomy terms admin page

Since WordPress does not create an admin page for managing terms of custom user taxonomies, you’ll need to create that. For the sake of simplicity and laziness, I just used the admin page WordPress already uses for other taxonomies.

The problem with this is that WordPress doesn’t recognize that the “Professions” admin page is a sub-menu of the “Users” admin page when viewing it. The sub-menu is placed correctly, but when clicking on it, the “Posts” menu is opened in the admin. I’d really love to see some WordPress core support for this.

Update: See the comment by James below to get a fix for this.

The following code is what I used, but you can definitely create a custom admin page for managing your terms.

/* Adds the taxonomy page in the admin. */
add_action( 'admin_menu', 'my_add_profession_admin_page' );

/**
 * Creates the admin page for the 'profession' taxonomy under the 'Users' menu.  It works the same as any
 * other taxonomy page in the admin.  However, this is kind of hacky and is meant as a quick solution.  When
 * clicking on the menu item in the admin, WordPress' menu system thinks you're viewing something under 'Posts'
 * instead of 'Users'.  We really need WP core support for this.
 */
function my_add_profession_admin_page() {

    $tax = get_taxonomy( 'profession' );

    add_users_page(
        esc_attr( $tax->labels->menu_name ),
        esc_attr( $tax->labels->menu_name ),
        $tax->cap->manage_terms,
        'edit-tags.php?taxonomy=' . $tax->name
    );
}

This will at least allow you to create and manage custom terms of the profession taxonomy. If you have a better solution for this, please post it in the comments for other people to see.

Fixing the “Posts” column

After creating the admin page, you probably noticed a column on it called “Posts.” Instead, it should display a “Users” column. The following code will fix this issue and list the number of users with each term from the profession taxonomy.

/* Create custom columns for the manage profession page. */
add_filter( 'manage_edit-profession_columns', 'my_manage_profession_user_column' );

/**
 * Unsets the 'posts' column and adds a 'users' column on the manage profession admin page.
 *
 * @param array $columns An array of columns to be shown in the manage terms table.
 */
function my_manage_profession_user_column( $columns ) {

    unset( $columns['posts'] );

    $columns['users'] = __( 'Users' );

    return $columns;
}

/* Customize the output of the custom column on the manage professions page. */
add_action( 'manage_profession_custom_column', 'my_manage_profession_column', 10, 3 );

/**
 * Displays content for custom columns on the manage professions page in the admin.
 *
 * @param string $display WP just passes an empty string here.
 * @param string $column The name of the custom column.
 * @param int $term_id The ID of the term being displayed in the table.
 */
function my_manage_profession_column( $display, $column, $term_id ) {

    if ( 'users' === $column ) {
        $term = get_term( $term_id, 'profession' );
        echo $term->count;
    }
}

Assigning terms to users

Thus far, you’ve got a custom taxonomy and can add terms for it, but you need a way to assign these terms to individual users. For this example, you’ll be adding a custom section to the edit user/profile page in the admin. It will display a list of radio select boxes so the user can choose their profession.

This is just an extremely basic example. You have tons of room for customization. You can do select elements, checkboxes, a text input, or whatever best works for you.

The following PHP code will add this section to the edit user/profile page.

/* Add section to the edit user page in the admin to select profession. */
add_action( 'show_user_profile', 'my_edit_user_profession_section' );
add_action( 'edit_user_profile', 'my_edit_user_profession_section' );

/**
 * Adds an additional settings section on the edit user/profile page in the admin.  This section allows users to
 * select a profession from a checkbox of terms from the profession taxonomy.  This is just one example of
 * many ways this can be handled.
 *
 * @param object $user The user object currently being edited.
 */
function my_edit_user_profession_section( $user ) {

    $tax = get_taxonomy( 'profession' );

    /* Make sure the user can assign terms of the profession taxonomy before proceeding. */
    if ( !current_user_can( $tax->cap->assign_terms ) )
        return;

    /* Get the terms of the 'profession' taxonomy. */
    $terms = get_terms( 'profession', array( 'hide_empty' => false ) ); ?>

    <h3><?php _e( 'Profession' ); ?></h3>

    <table class="form-table">

        <tr>
            <th><label for="profession"><?php _e( 'Select Profession' ); ?></label></th>

            <td><?php

            /* If there are any profession terms, loop through them and display checkboxes. */
            if ( !empty( $terms ) ) {

                foreach ( $terms as $term ) { ?>
                    <input type="radio" name="profession" id="profession-<?php echo esc_attr( $term->slug ); ?>" value="<?php echo esc_attr( $term->slug ); ?>" <?php checked( true, is_object_in_term( $user->ID, 'profession', $term ) ); ?> /> <label for="profession-<?php echo esc_attr( $term->slug ); ?>"><?php echo $term->name; ?></label> <br />
                <?php }
            }

            /* If there are no profession terms, display a message. */
            else {
                _e( 'There are no professions available.' );
            }

            ?></td>
        </tr>

    </table>
<?php }

After adding the preceding code, you’ll get a new section near the bottom of your edit user/profile page that looks like the following screenshot.

Assigning taxonomy terms on user profile page

You’ll need a way to save the selected term (profession) for the user. The next code block will handle that.

/* Update the profession terms when the edit user page is updated. */
add_action( 'personal_options_update', 'my_save_user_profession_terms' );
add_action( 'edit_user_profile_update', 'my_save_user_profession_terms' );

/**
 * Saves the term selected on the edit user/profile page in the admin. This function is triggered when the page
 * is updated.  We just grab the posted data and use wp_set_object_terms() to save it.
 *
 * @param int $user_id The ID of the user to save the terms for.
 */
function my_save_user_profession_terms( $user_id ) {

    $tax = get_taxonomy( 'profession' );

    /* Make sure the current user can edit the user and assign terms before proceeding. */
    if ( !current_user_can( 'edit_user', $user_id ) && current_user_can( $tax->cap->assign_terms ) )
        return false;

    $term = esc_attr( $_POST['profession'] );

    /* Sets the terms (we're just using a single term) for the user. */
    wp_set_object_terms( $user_id, array( $term ), 'profession', false);

    clean_object_term_cache( $user_id, 'profession' );
}

Displaying user taxonomy terms

You can use any of the standard WordPress taxonomy functions for listing out your terms. For example, wp_list_categories() and wp_tag_cloud() will both work fine. Each will link to the term archive page (covered in the section below).

If you wanted to show a tag cloud with all of your “professions” (profession cloud) in a theme template, your code would look something like the following.

<?php wp_tag_cloud( array( 'taxonomy' => 'profession' ) ); ?>

One thing to look out for though is that the title attribute for links in the tag cloud will read “1 topic” or “X topics.” An easy way to change this is to write a custom callback function to modify the text. In the following screenshot, you can see this changed from “topics” to “users”.

Tooltip changed to 'users' on term archive link

The following code block is a function that will handle that.

/**
 * Function for outputting the correct text in a tag cloud.  Use as the 'update_topic_count_callback' argument
 * when calling wp_tag_cloud().  Instead of 'topics' it displays 'users'.
 *
 * @param int $count The count of the objects for the term.
 */
function my_profession_count_text( $count ) {
    return sprintf( _n('%s user', '%s users', $count ), number_format_i18n( $count ) );
}

You’d use it when displaying your tag cloud like so:

<?php wp_tag_cloud(
    array(
        'taxonomy' => 'profession',
        'topic_count_text_callback' => 'my_profession_count_text'
    )
); ?>

Templates for term archives

User taxonomy archive page

The template for term archives is handled the same as any other taxonomy. So, if you wanted to create a custom template (you probably should) for your theme to display users by profession, you’ll want to create a template named taxonomy-profession.php.

I won’t cover creating theme templates here. It’s outside the scope of this tutorial.

The big difference between this template and a normal template is there’s no post loop. Instead, you need to create a user loop. The following code should replace your normal post loop in this template.

<?php
$term_id = get_queried_object_id();
$term = get_queried_object();

$users = get_objects_in_term( $term_id, $term->taxonomy );

if ( !empty( $users ) ) {
?>
    <?php foreach ( $users as $user_id ) { ?>

        <div class="user-entry">
            <?php echo get_avatar( get_the_author_meta( 'email', $user_id ), '96' ); ?>
            <h2 class="user-title"><a href="<?php echo esc_url( get_author_posts_url( $user_id ) ); ?>"><?php the_author_meta( 'display_name', $user_id ); ?></a></h2>

            <div class="description">
                <?php echo wpautop( get_the_author_meta( 'description', $user_id ) ); ?>
            </div>
        </div>

    <?php } ?>
<?php } ?>

The most important function used in the code above is the WordPress get_objects_in_term() function. By putting in a term ID and taxonomy name, you can grab an array of all the object (user) IDs with that term (profession). With the user ID, you can load any information about a user you want with any standard WordPress functions.

Of course, you’re free to customize this however you want. The preceding code merely loops through each of the users with a specific profession. It then displays each user’s avatar, name with a link to their posts archive, and description.

Disabling the ‘profession’ username

When you registered the profession taxonomy, you created the slug author/profession so that the profession archive pages would have a URL like yoursite.com/author/profession/designer. The problem with this that “profession” could potentially be a username someone signs up to your site with.

The following code will make sure no one can sign up with this username:

/* Filter the 'sanitize_user' to disable username. */
add_filter( 'sanitize_user', 'my_disable_username' );

/**
 * Disables the 'profession' username when someone registers.  This is to avoid any conflicts with the custom
 * 'author/profession' slug used for the 'rewrite' argument when registering the 'profession' taxonomy.  This
 * will cause WordPress to output an error that the username is invalid if it matches 'profession'.
 *
 * @param string $username The username of the user before registration is complete.
 */
function my_disable_username( $username ) {

    if ( 'profession' === $username )
        $username = '';

    return $username;
}

Of course, if you use a different rewrite structure for your taxonomy, don’t worry about that code.

Time to create your own user taxonomies

Whoah! That was certainly a ton of information to cover in a single tutorial. I’m sure some of you have questions, so please ask away in the comments.

Now it’s time for you to venture out on your own and create some cool stuff. I’d love to hear what ideas you have and see any projects that you use this code in. I’m definitely interested in seeing some practical, real-world use cases of user taxonomies.

One final note: The code in this tutorial is just something I played around with in about a two-hour span. It’s still a work in progress. Honestly, it took me much longer to write this tutorial. Therefore, I leave it up to you, dear reader, to improve upon the code.