Can One Taxonomies Terms be Ordered by A Seperate Taxonomy?

As I said, this is doable, but we need to do careful planning as this is quite a heavy operation. On my test installation with a post count of just 13 posts, and 3 terms per taxonomy, the db is visited 20 times and the complete operation takes 0.03613 seconds. I have tried a couple of solutions, and this one is by far the fastest.

We will look at a workaround later to save on db calls and load time. Lets first see how the whole operation looks like and how it works

PREPHASE

Just a few notes before we start. This code is formulated to work based on the following

  • Each post must have at least one term in both of the two taxonomies. If a post only has a term belonging to one of the two taxonomies, the code will fail

  • On my test install, my post type is cameras, and my taxonomies are brands and event_cat, so be sure to change this accordingly

HOW IT WORKS

STEP 1

Add your taxonomies in an array. This will be used to get all the terms associated with these two taxonomies

$taxonomies = [ 'event_cat', 'brands' ];

STEP 2

As stated in step 1, you need to get all the terms per taxonomy. get_terms() will be used to retrieve the terms. Empty terms will be skipped

$terms = get_terms( $taxonomy );

STEP 3

The next step will be to get all the posts that belongs to the specific term. The term slug and taxonomy name will be used in a tax_query in get_posts to retrieve the posts. As you don’t need any post data, you are just going to retrieve the post ID’s

$args = array(
    'post_type' => 'cameras',
    'fields' => 'ids',
    'tax_query' => array(
        array(
            'taxonomy' => $taxonomy,
            'field'    => 'slug',
            'terms'    => $term->slug,
        ),
    ),
);
$posts = get_posts( $args );

STEP 4

The post ID, term name and taxonomy name will now be used to create an array which will be used to create our list

foreach ( $posts as $post ) {
    $all_posts[$post][$taxonomy] = $term->name;
}

Your create array will look something like this

array(13) {
  [586]=>
  array(2) {
    ["event_cat"]=>
    string(6) "3 star"
    ["brands"]=>
    string(7) "Classic"
  }
  [582]=>
  array(2) {
    ["event_cat"]=>
    string(6) "3 star"
    ["brands"]=>
    string(7) "Romance"
  }
  [331]=>
  array(2) {
    ["event_cat"]=>
    string(6) "3 star"
    ["brands"]=>
    string(7) "Classic"
  }
  [329]=>
  array(2) {
    ["event_cat"]=>
    string(6) "3 star"
    ["brands"]=>
    string(6) "Comedy"
  }
  [585]=>
  array(2) {
    ["event_cat"]=>
    string(6) "4 star"
    ["brands"]=>
    string(7) "Romance"
  }
  [583]=>
  array(2) {
    ["event_cat"]=>
    string(6) "4 star"
    ["brands"]=>
    string(7) "Romance"
  }
  [401]=>
  array(2) {
    ["event_cat"]=>
    string(6) "4 star"
    ["brands"]=>
    string(7) "Classic"
  }
  [330]=>
  array(2) {
    ["event_cat"]=>
    string(6) "4 star"
    ["brands"]=>
    string(6) "Comedy"
  }
  [328]=>
  array(2) {
    ["event_cat"]=>
    string(6) "4 star"
    ["brands"]=>
    string(6) "Comedy"
  }
  [587]=>
  array(2) {
    ["event_cat"]=>
    string(6) "5 star"
    ["brands"]=>
    string(7) "Romance"
  }
  [584]=>
  array(2) {
    ["event_cat"]=>
    string(6) "5 star"
    ["brands"]=>
    string(7) "Romance"
  }
  [581]=>
  array(2) {
    ["event_cat"]=>
    string(6) "5 star"
    ["brands"]=>
    string(6) "Comedy"
  }
  [327]=>
  array(2) {
    ["event_cat"]=>
    string(6) "5 star"
    ["brands"]=>
    string(6) "Comedy"
  }
}

STEP 5

This new array will be the basis of the list that will be displayed. The first thing to do with this array is to use it to create a new array which will use the ratings terms as keys and the genre terms as the values

$group_ratings = [];
foreach ( $all_posts as $value ) {
    $group_ratings[$value['event_cat']][] = $value['brands'];
}

Your $group_ratings will now look like this

array(3) {
  ["3 star"]=>
  array(4) {
    [0]=>
    string(7) "Classic"
    [1]=>
    string(7) "Romance"
    [2]=>
    string(7) "Classic"
    [3]=>
    string(6) "Comedy"
  }
  ["4 star"]=>
  array(5) {
    [0]=>
    string(7) "Romance"
    [1]=>
    string(7) "Romance"
    [2]=>
    string(7) "Classic"
    [3]=>
    string(6) "Comedy"
    [4]=>
    string(6) "Comedy"
  }
  ["5 star"]=>
  array(4) {
    [0]=>
    string(7) "Romance"
    [1]=>
    string(7) "Romance"
    [2]=>
    string(6) "Comedy"
    [3]=>
    string(6) "Comedy"
  }
}

STEP 6

As you need the 5 star term to appear first and 1 star term last, we can use krsort to sort the array accordingly

krsort($group_ratings);

STEP 7

The array will now be split through a foreach loop. Each array value will now be counted with array_count_values, then the resulting array will be sorted by array value and key value with array_multisort. The sorting gave me a bit of a headache, so I had to go and look for some help. I found it here thanks to @theark

foreach ( $group_ratings as $key=>$value ) {
    echo $key;
    $counted_values = array_count_values($value);
    array_multisort(array_values($counted_values), SORT_DESC, array_keys($counted_values), SORT_ASC, $counted_values);
//MORE TO COME

STEP 8

Finally the list can now be displayed. The array keys which holds the term names will be used as the term name, and the array value which holds the term post count will be used to display the post count for that particular term

foreach ( $counted_values as $counted_values_keys=>$counted_value ) {
    echo '<li>' . $counted_values_keys . ' (' . $counted_value . ') </li>';
}

As I said, this operation is quite heavy. To make this faster, you need to make use of transients. What we are going to do is to store the result from $group_ratings. This is the variable that hold all the important information

This will significantly reduce the amount of db calls and load time. With the transient, only 2 db hits are recorded, and total time spend is only 0.00098 seconds. Just a note, I have set the transient expiry time for 24 hours, you can modify this as needed. A good time to set will be determined on how often do you add new posts

ALL TOGETHER NOW!!

Here is how the completed code looks like

if ( false === ( $group_ratings = get_transient( 'term_list' ) ) ) {
    $taxonomies = [ 'event_cat', 'brands' ];
    $all_posts = [];
    foreach ( $taxonomies as $taxonomy ) {
        $terms = get_terms( $taxonomy );
        foreach ( $terms as $term ) {
            $args = array(
                'post_type' => 'cameras',
                'fields' => 'ids',
                'tax_query' => array(
                    array(
                        'taxonomy' => $taxonomy,
                        'field'    => 'slug',
                        'terms'    => $term->slug,
                    ),
                ),
            );
            $posts = get_posts( $args );

            foreach ( $posts as $post ) {
                $all_posts[$post][$taxonomy] = $term->name;
            }
            unset($post);
        }
    }

    $group_ratings = [];
    foreach ( $all_posts as $value ) {
        $group_ratings[$value['event_cat']][] = $value['brands'];
    }

    krsort($group_ratings);

    set_transient( 'term_list', $group_ratings, 24 * HOUR_IN_SECONDS );
}


echo '<ul>';
foreach ( $group_ratings as $key=>$value ) {
    echo $key;
    $counted_values = array_count_values($value);
    array_multisort(array_values($counted_values), SORT_DESC, array_keys($counted_values), SORT_ASC, $counted_values);

    foreach ( $counted_values as $counted_values_keys=>$counted_value ) {
        echo '<li>' . $counted_values_keys . ' (' . $counted_value . ') </li>';
    }
}
echo '</ul>';

You will just need to style and modify it as needed. Here is how your list will look like

enter image description here

BUT WAIT, WE’RE NOT DONE

There are one last final step. The transient will only be updated if the transient expires which will be a problem if you publish new posts within during the time the transient is valid. You changes will only show after the transient expires and are updated.

What you need to do is to somehow delete the transient when a new post is published. This can be done using the transition_post_status hook as described here by @tosho in this post

We need to slightly modify that code so that the transient only gets deleted when a new post is published and also just when a new post is published in the particular post type. This is how that code looks like (this goes into functions.php, and just remember to change the post type name)

add_action( 'transition_post_status', 'a_new_post', 10, 3 );

function a_new_post( $new_status, $old_status, $post )
{
    if ( 'publish' !== $new_status or 'publish' === $old_status )
        return;

    if ( 'cameras' !== $post->post_type )
        return; // restrict the filter to a specific post type

    delete_transient( 'term_list' );
}

WE’RE DONE!!!