Registering Custom Taxonomies for Custom Post Types the right way

A project that I’m currently working on, involves around a dozen custom post types and custom taxonomies. To make all those archives readable, I had to introduce forms that allow the filtering of those archives to make them easier searchable. When you’ve to deal with such a task, then you’ll pretty quickly reach the point where you realize that internals that are relying on query vars will fail.

In detail, most of what relies on get_queried_object() will fail as the queried object is one object and doesn’t consider everything that is actually part of the query. But there’s help: We can utilize the parse_query or pre_get_posts to set some things like is_post_type_archive to true manually.

To get around this, I built some mini-plugins that will help me handling those cases for specific post type and taxonomy archives. Inside those plugins __construct() methods, I added my filter callbacks. But when I dumped globals like $wp_post_types, that I needed to check against the $query argument in those filter callbacks, I soon realized that I had one big problem: The admin UI showed me that I registered custom taxonomies for custom post types, but those CPTs hadn’t got the taxonomies attached. Take a look at the following (shortened) dump of the post type object and its empty taxonomies array.

  public 'labels' => 
  public 'description' => string '' (length=0)
  // (...)
  public 'rewrite' => 
    array (size=5)
      // (...)
  public 'has_archive' => boolean true
  public 'query_var' => string 'example' (length=5)
  public 'register_meta_box_cb' => null
  public 'taxonomies' => 
    array (size=0)
  public 'show_ui' => boolean true
  // (...)
  public 'cap' => 
  public 'label' => string 'Example Post Type' (length=6)

So I was back at the drawing table with a new rule.

Always register your CPTs and CTs as early as possible. And assign them to each other.

Now I went through code and assigned both the methods that register the custom post types as the custom taxonomies on the init hook with a priority of 0. Then I added the taxonomies key to the arguments array for register_post_type( $name, $args = array() );. I did the same for register_taxonomy( $name, $cpts = array() );. Suddenly the post type and taxonomy objects had each other attached.

And all this brought me the following possibility: Trigger my filters/methods only when I got a hierarchical taxonomy.

public function __construct()
	add_filter( 'parse_query', array( $this, 'query' ) );

public function get_hierarchical()
	global $wpdb;
	$taxonomies = $wpdb->get_col( "
		SELECT DISTINCT tt.taxonomy
		FROM {$wpdb->term_taxonomy} AS tt
	" );

	$results = array();
	foreach ( $taxonomies as $tax )
		// Check if we really got a hierarchical tax
		false !== get_option( "{$tax}_children" )
			AND $results[] = $tax;

	return $this->hierarchical = $results;

public function query( $query )
	// Check if the current query has a hierarchical taxonomy queried
	$process = false;
	foreach ( $this->hierarchical as $tax )
		if ( null !== $query->get( $tax ) )
			$process = true;
	// Grab those post types that have taxonomies attached
	$post_types = array_filter( wp_list_pluck(
	) );
	$allowed_post_types = array();
	foreach ( $post_types as $pt => $taxonomies )
		foreach ( $taxonomies as $t )
			if ( in_array( $t, $this->hierarchical ) )
				$allowed_post_types[] = $pt;

	// Abort scenarios
	if (
		! $process
		OR ! $query->is_archive()
		OR ! $query->is_post_type_archive( $allowed_post_types )
		return $query;


	return $query;

This is just one example of what you can check against, to trigger your callbacks in specific cases. And when you can’t rely on the $wp_query queried object anymore and have to manually set values to true|false, then it will come in handy that you already have your custom post types and custom taxonomies set up and attached/interlinked to each other.


Web developer, designer, snow and mountain addict from Austria. Founder of wecodemore.

Find kaiser on⁠, ⁠, ⁠, ⁠, and .

Leave a Reply

Your email address will not be published.