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 arebrands
andevent_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
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!!!