3
votes

I've got a custom post type called "products", which has two custom taxonomies - "product-range" and "product-categories". The range serves as a top-level grouping, whereas the category is a sub-grouping within that.

I've set-up a taxonomy-product-range.php template which features the following code:

<?php
$terms = get_terms('product-categories');
foreach( $terms as $term ):
?>     

<h2><?php echo $term->name;?></h2>
<ul>

    <?php                         
    $posts = get_posts(array(
        'post_type' => 'products',
        'taxonomy' => $term->taxonomy,
        'term' => $term->slug,
        'nopaging' => true
    ));
    foreach($posts as $post): setup_postdata($post);
    ?>

    <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>

    <?php endforeach; ?>

</ul>           

<?php endforeach; ?>

This works as expected by outputting the products and grouping them by product category. However, it outputs all of the products, regardless of which archive you're viewing. I need it to only output posts for the archive you're viewing.

This feels like it's nearly there, but I'm not sure how to fix it.

== Edit ==

Each of the products will belong to one "Range" and one "Category". When people visit the product-range archive page, I'm trying to display the following:

<h1>Range Title</h1>
<h2>Category 1 Title</h2>
<ul>
<li>Product 1 Title</li>
<li>Product 2 Title</li>
<li>Product 3 Title</li>
</ul>
<h2>Category 2 Title</h2>
<ul>
<li>Product 4 Title</li>
<li>Product 5 Title</li>
<li>Product 6 Title</li>
</ul>
1
@CayceK no it is not. That will not work in this casePieter Goosen
@PieterGoosen not meant for an answer as to why I didn't post answer but it is related in the idea of the mater. I only post it so it can be seen if someone asks this question and this does not solve them :DCayce K
@CayceK seems I have to prove a point here with sorting. Remember, 10 terms means 10 custom queries, 1000 terms means 1000 custom queries. See how expensive it getsPieter Goosen
@PieterGoosen yes those are true. Keep in mind I didn't mark as duplicate I just said may be related. Again that related has nothing to do to trying to solve. It also has nothing to do with suggesting a way to do this specific. However, if another user attempts to find an answer to their question they may need to be linked to that question as it may be more correct for them. It is not my attempt to say that anything here is wrong or anything here is better. It is only meant to provide a secondary source of maybe this will help someone else. Also the use of "Possibly" is key to this thought.Cayce K

1 Answers

4
votes

Simple remove the code that you have and replace it with the default loop. You should not replace the main query with a custom one. Use pre_get_posts to alter the main query according to needs.

This is what your taxonomy page should look like

if ( have_posts() ) {
    while ( have_posts() ) {
    the_post();

        // Your template tags and markup

    }
}

As your problem is sorting, we will tackle this using usort and thee the_posts filter to do the sorting before the loop runs but just after the main query has run. We will not use multiple loops as they are quite expensive and resource intensive, and it breaks page functionalities

I have commented the code so it can be easy to follow and understand. (NOTE: The following code is untested and requires PHP 5.4+ due to array dereferencing)

add_filter( 'the_posts', function ( $posts, $q ) 
{
    $taxonomy_page = 'product-range';
    $taxonomy_sort_by = 'product-categories'; 

    if (    $q->is_main_query() // Target only the main query
         && $q->is_tax( $taxonomy_page ) // Only target the product-range taxonomy term pages
    ) {
        /**
         * There is a bug in usort that will most probably never get fixed. In some instances
         * the following PHP warning is displayed 

         * usort(): Array was modified by the user comparison function
         * @see https://bugs.php.net/bug.php?id=50688

         * The only workaround is to suppress the error reporting
         * by using the @ sign before usort
         */      
        @usort( $posts, function ( $a, $b ) use ( $taxonomy_sort_by )
        {
            // Use term name for sorting
            $array_a = get_the_terms( $a->ID, $taxonomy_sort_by );
            $array_b = get_the_terms( $b->ID, $taxonomy_sort_by );

            // Add protection if posts don't have any terms, add them last in queue
            if ( empty( $array_a ) || is_wp_error( $array_a ) ) {
                $array_a = 'zzz'; // Make sure to add posts without terms last
            } else {
                $array_a = $array_a[0]->name;
            }

            // Add protection if posts don't have any terms, add them last in queue
            if ( empty( $array_b ) || is_wp_error( $array_b ) ) {
                $array_b = 'zzz'; // Make sure to add posts without terms last
            } else {
                $array_b = $array_b[0]->name;
            }

            /**
             * Sort by term name, if term name is the same sort by post date
             * You can adjust this to sort by post title or any other WP_Post property_exists
             */
            if ( $array_a != $array_b ) { 
                // Choose the one sorting order that fits your needs
                return strcasecmp( $array_a, $array_b ); // Sort term alphabetical ASC 
                //return strcasecmp( $array_b, $array_a ); // Sort term alphabetical DESC
            } else {
                return $a->post_date < $b->post_date; // Not sure about the comparitor, also try >
            }
        });
    }
    return $posts;
}, 10, 2 ); 

EDIT

Here is how your loop should look like to display your page in the order in your edit

if ( have_posts() ) {
    // Display the range term title
    echo '<h1>' . get_queried_object()->name . '</h1>';

    // Define the variable which will hold the term name
    $term_name_test = '';

    while ( have_posts() ) {
    the_post();

        global $post;
        // Get the terms attached to a post
        $terms = get_the_terms( $post->ID, 'product-categories' );
        //If we don't have terms, give it a custom name, else, use the first term name
        if ( empty( $terms ) || is_wp_error( $terms ) ) {
            $term_name = 'SOME CUSTOM NAME AS FALL BACK';
        } else { 
            $term_name = $terms[0]->name;
        }

        // Display term name only before the first post in the term. Test $term_name_test against $term_name
        if ( $term_name_test != $term_name ) {
            // Close our ul tags if $term_name_test != $term_name and if not the first post
            if ( $wp_query->current_post != 0 )
                echo '</ul>';

            echo '<h2>' . $term_name . '</h2>';

            // Open a new ul tag to enclose our list
            echo '<ul>';
        } // endif $term_name_test != $term_name

        $term_name_test = $term_name;

        echo '<li>' . get_the_title() . '</li>';    

        // Close the ul tag on the last post        
        if ( ( $wp_query->current_post + 1 ) == $wp_query->post_count ) 
            echo '</ul>';

    }
}

EDIT 2

The code above is now tested and working. On request, here are the test run on my local install. For this test I have used the code in OP and my code.

RESULTS

(This results was obtained with the Query Monitor Plugin. Also, all results include the same extra queries made by widgets, nav menus, custom functions etc)

  • Code in OP -> 318 db queries in 0.7940 s with page generation time of 1.1670s. Memory usage was 12.8Mb

  • My code in answer -> 46 db queries in 0.1045 s with page generation time of 0.1305s. Memory usage was 12.6Mb

As I have stated previously, the proof is in the pudding