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.
object(stdClass)[84]
public 'labels' =>
object(stdClass)[86]
(...)
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' =>
object(stdClass)[85]
(...)
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()
{
get_hierarchical();
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(
$GLOBALS['wp_post_types']
,'taxonomies'
) );
$allowed_post_types = array();
foreach ( $post_types as $pt => $taxonomies )
{
foreach ( $taxonomies as $t )
{
if ( in_array( $t, $this->hierarchical ) )
$allowed_post_types[] = $pt;
break;
}
}
// Abort scenarios
if (
! $process
OR ! $query->is_archive()
OR ! $query->is_post_type_archive( $allowed_post_types )
)
return $query;
// DO STUFF
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.