0

I have a function that I am using to number posts in a category. It is in my function.php and looks like this:

function get_episode_number($postID){
$options = get_option('theme-opts'); 
$opts_cat = isset($options['opts-category']) ? $options['opts-category'] : ''; /* passes category from theme options */

$postNumberQuery = new WP_Query(array ( 
    'orderby' => 'date', 
    'order' => 'ASC', 
    'post_type' => 'post',
    'posts_per_page' => '-1', 
    'cat' => $opts_cat 
));
$counter = 1;
$postCount = 0;

if( $postNumberQuery->have_posts() ) :
    while ( $postNumberQuery->have_posts() ) :
        $postNumberQuery->the_post();
        if ( $postID == get_the_ID() ){
            $postCount = $counter;
        } else {
            $counter++;
        }
    endwhile;
endif;
wp_reset_postdata();
return $postCount;
}

When I use this function within another custom wp_query loop it's wp_reset_postdata() seems to not only reset the query in the custom function but the query i am using it in as well. Here is the code:

<?php 
        $paged = ( get_query_var( 'page' ) ) ? absint( get_query_var( 'page' ) ) : 1;

        $l_args = array(
            'cat' => $opts_cat,
            'post_type' => 'post',
            'posts_per_page' => 2,
            'paged' => $paged,
        );
        $landing_p = new WP_Query( $l_args );
        /* The following line creates the main loop. */
        if ($landing_p->have_posts()) : while ($landing_p->have_posts()) : $landing_p->the_post(); ?>            
        <div id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
            <div class="inside">
                <?php the_post_thumbnail('square'); ?>

                <div class="hover bottom">
                    <?php
                        $currentID = get_the_ID(); ?>
                    <?php $episodeNum = get_episode_number($currentID); ?>
                    <span class="episode"><?php echo __('Episode', 'lang'); ?> <?php echo $episodeNum; ?></span>
                    <a href="<?php the_permalink(); ?>"><h2><?php the_title(); ?></h2></a> 
                </div><!-- .hover-bottom -->
            </div><!-- .inside -->
        </div><!-- .post -->
        <?php 
        endwhile;
        wp_reset_postdata();
        endif; ?>

The the_permalink() and the_title() aren't being retrieved from the $landing_p query anymore. Instead they display the title and permalink of the page. This only happens when I use get_episode_number() . So wp_reset_postdata() seems to be resetting both queries. My question is, what is the scope of wp_reset_postdata() from the function and is the a way of making sure it's effect doesn't 'overflow' for the lack of a better word. Any pointers are very welcome. Thanks!

9
  • If you are using a WP_Query within another WP_Query then you are doing something wrong instead Nov 21 '14 at 10:07
  • Is there a rule against using a WP_Query within another WP_Query? I'm asking because I really don't know and didn't seem to come across anything like that while searching. Nov 21 '14 at 10:14
  • Can't think of any rule forbidding this, but you have to remember, any instance of WP_Query changes the $post global. That happens on every post. $post gets updated on every post and will eventually hold the last post value. This creates one big mess when running one instance of WP_Query within another. Nov 21 '14 at 10:21
  • I see, thanks for that info, it makes sense. The thing is, I'm trying to number the posts within a category. As in, below each title it would say Episode 1 or Episode 2,... and so on. I'm trying to avoid custom meta fields and simply count the posts. This is the only solution I found so far. Do you have any idea how I could possibly do this another way? Nov 21 '14 at 10:27
  • You should rather use get_posts in your function to count your posts. Also, is it really necessary to run a custom query in your second block of code. Can't the main query be used in conjuction with pre_get_posts Nov 21 '14 at 10:29
1

The answer from the OP inspired me to quickly write this function which is not resource intensive and really fast. I also decided to try another method as described by the OP

To accomplish this, I've made use of the Transient API and the post_updated action

THE LOGIC

To get the position of a post, you will first need to get all the posts and determine where in the array the post is and add 1 to its current position/key (Remember, an array starts at 0

This is quite an expensive operation when you are doing this on each page load, and it can become really slow if you have several hundred or thousands of posts

So my idea here was to use a transient to store the resultant post position. I have made the transient to expire in 365 days, you can just set it to your liking.

My decision to make it this long comes from the following idea:

A specific post's position will only change when:

  • A post before it has been deleted or

  • A deleted post before it has been undeleted

You can however extend this if you wish to include other changes in a post's status. I would however exclude when a new post is published. This will never have any effect on previous posts

There is a need to delete all the post positions whenever a post is deleted or undeleted and give each post a new post position. So this means you have to delete the current transients and replace it with a new value. The new value will be set when the post is opened for the first time or when the transient does not exist.

My SQL is not great, so I borrowed code from this answer to delete all the transients whenever a post id deleted or undeleted

HERE IS THE CODE

(I have commented the code to explain what happens and why.)

All of this goes into your functions.php

function display_post_number() {

    $query_object   = get_queried_object(); // More safe that $post global
    $id             = $query_object->ID;
    $transient_id   = 'post_number_' . $id; // Give every post a unique transient

    if ( false === ( $post_number = get_transient( $transient_id ) ) ) {
        $post_args = [ 
            'post_type'         => $query_object->post_type, // Set the current post's type as post type
            'fields'            => 'ids', // Only get post ID's
            'posts_per_page'    => -1,
            'order'             => 'ASC', //Get posts from first to last
        ];

        $q = get_posts( $post_args );

        $post_number = array_search( $id, $q ) + 1; //Use array_search to get post position and add 1 

        set_transient( $transient_id, $post_number, 365 * DAY_IN_SECONDS );
    }

    echo $post_number;
}

add_action( 'post_updated', function ( $post, $post_after, $post_before ) {

   if ( 
        $post_after->post_status == 'trash' && $post_before->post_status == 'publish'
        || $post_after->post_status == 'publish' && $post_before->post_status == 'trash'   
    ) { // Only delete the transients if a post is deleted or undeleted
        global $wpdb;
        $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient%_post_number_%')" );
        $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient_timeout%_post_number_%')" );
   }

}, 10, 3 );

You can then use display_post_number() in your single.php to display the post number

EDIT 1

If you need to show something like Post X of Y you can adjust the code as follows.

function display_post_number() {

    $query_object   = get_queried_object(); // More safe that $post global
    $id             = $query_object->ID;
    $transient_id   = 'post_number_' . $id; // Give every post a unique transient

    if ( false === ( $text = get_transient( $transient_id ) ) ) {
        $post_args = [ 
            'post_type'         => $query_object->post_type, // Set the current post's type as post type
            'fields'            => 'ids', // Only get post ID's
            'posts_per_page'    => -1,
            'order'             => 'ASC', //Get posts from first to last
        ];

        $q = get_posts( $post_args );

        $total_posts = count( $q ); // Get total posts
        $post_number = array_search( $id, $q ) + 1; //Use array_search to get post position and add 1 

        $text = 'You are currently viewing post #' . $post_number . ' of ' . $total_posts;

        set_transient( $transient_id, $text, 365 * DAY_IN_SECONDS );
    }

    echo $text;

}

Then, instead of using the post_updated hook, make use of the transition_post_status to delete the post positions whenever a post's status change as everytime a post status change, the amount of posts will changes.

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

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

        global $wpdb;
        $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient%_post_number_%')" );
        $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient_timeout%_post_number_%')" );

}, 10, 3 );

EDIT 2

From your comments

what would be a good method of finding out which operations are "expensive"? I love myself some lean, fast code, but it's not always easy to spot what might be slowing things down. Generally speaking, what should one keep and eye on/avoid?

There is really no general rule. To see what I mean, see this post I have just done. In that post I have showed that four custom queries is faster than a single query in that particular situation, but in another situations it can be reversed. So, it all depends on situation and what you want to do. As I always say, 100 well constructed queries can be faster than one lump query

The only way to know is to test, and this is really easy. I have actually created myself a test page on my dev site just for testing. You can use any page/template. Here is my page

<?php
/**
 * Template Name: TEST Page
 */
get_header(); ?>

<div id="main-content" class="main-content">

    <div id="primary" class="content-area">
        <div id="content" class="site-content" role="main">


<?php

timer_start();      
/**-----------------------------------------------------------------------------
 *
 *  DON'T EDIT ABOVE THIS LINE
 *
*------------------------------------------------------------------------------*/   ?>
<?php

// Add all your code to test in this section

?>
<?php /**-------------------------------------------------------------------------
 *
 *  DON'T EDIT BELOW THIS LINE
 *
*------------------------------------------------------------------------------*/   ?>  
<p><?php echo get_num_queries(); ?> queries in <?php timer_stop(1, 5); ?> seconds. </p>

        <!--#page-content-->

        </div><!-- #content -->
    </div><!-- #primary -->
    <?php get_sidebar( 'content' ); ?>
</div><!-- #main-content -->

<?php
get_footer();

The functions that are important here are the following which you can read up on

When you fist set up your page, run it and record the default amount of queries. On my test site I get 11 queries in 0.00000 seconds., so I know I need to subtract 11 from all my test data. (SIDENOTE: Don't test with cache plugins installed and also never test with transients. The reason is, once a transient is set, you will need to manually remove it from the database on every page refresh which is quite a shlep. Always add it last)

I've done couple of test with a post count of 26 posts, and here are the results. Just a few quick notes, I have removed the transient from my code to test, and I have already subtracted 11 from the results that I have pasted here. I have also removed the category parameter.

TEST 1

Your code were I added updateNumbers(); in place of // Add all your code to test in this section

Result - 83 queries in 0.13476 seconds.

TEST 2

The linked code from your answer

Result - 133 queries in 0.16016 seconds.

TEST 3

My code without the transient with display_post_number().

Result - 1 queries in 0.00293 seconds.

TEST 4

My code with the transient. Note the extra query, but the reduction in time, so, as you can see, 2 queries can be faster than one :-)

Result - 2 queries in 0.00098 seconds.

TO CONCLUDE

Less is not always faster. The only way to really know is to test, and as you can see, the proof is in the numbers. I hope this helps you in fututre

2
  • Thanks for working this out and writing all this down. I had never heard of transients until now and this makes a lot of sense. I'll test it and report back. Btw, what would be a good method of finding out which operations are "expensive"? I love myself some lean, fast code, but it's not always easy to spot what might be slowing things down. Generally speaking, what should one keep and eye on/avoid? Nov 24 '14 at 9:06
  • Please see my update. I hope this helps :-) Nov 24 '14 at 17:45
1

Thanks @PieterGoosen and @Milo for the suggestion of doing everything back-end.

Found an answer here and adjusted it to use get_posts and suit my needs. My final code is:

function updateNumbers() {
$options = get_option('theme-opts'); 
$opts_cat = isset($options['opts-category']) ? $options['opts-category'] : '';
$args = array( 
    'posts_per_page' => -1, 
    'category' => $opts_cat,
    'orderby' => 'date',
    'order' => 'ASC',
    'post_type' => 'post'
);
$pageposts = get_posts( $args );
$counts = 0 ;
if ($pageposts):
    foreach ($pageposts as $post):
        setup_postdata($post);
        $counts++;
        add_post_meta($post->ID, 'incr_number', $counts, true);
        update_post_meta($post->ID, 'incr_number', $counts);
    endforeach;
endif;
}  
add_action ( 'publish_post', 'updateNumbers' );
add_action ( 'deleted_post', 'updateNumbers' );
add_action ( 'edit_post', 'updateNumbers' );

And then <?php echo get_post_meta($post->ID,'incr_number',true); ?> to display the number. Thanks again!

1
  • 1
    if you're just using the post ID, you can set the fields parameter to ids and your query will return just an array of post IDs, it would use less memory and simplify your code a bit.
    – Milo
    Nov 21 '14 at 20:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.