Is it possible to completely stop WP_Query retrieving posts?

The question:

I’m trying to use WP Redis to cache entire $wp_query object with key is $query_vars_hash.

This is how $wp_query was added to $wp_object_cache:

add_action('wp', function($wp)
{
    if ( is_admin() ) return;

    global $wp_query;

    if ( !wp_cache_get($wp_query->query_vars_hash, 'globals') )
    {
        wp_cache_add($wp_query->query_vars_hash, $wp_query, 'globals');
    }
});

Then, I need to check if a query has already cached before WP_Query can retrieve posts:

add_action('pre_get_posts', function($query)
{
    if ( is_admin() ) return;

    $cached_query = wp_cache_get($query->query_vars_hash, 'globals');

    if ($cached_query)
    {
        $GLOBALS['wp_query'] = &$cached_query;

        return; // Return immediately to prevent retrieving posts again.
    }
});

Problem:

return or exit doesn’t work in this case. Then, WP_Query will still hit database to retrieve posts again.

Question:

Regardless of the plugin, is it possible to completely stop WP_Query retrieving posts?

The Solutions:

Below are the methods you can try. The first solution is probably the best. Try others if the first one doesn’t work. Senior developers aren’t just copying/pasting – they read the methods carefully & apply them wisely to each case.

Method 1

At the moment, it is not possible.

When 'pre_get_posts' runs, is too late to stop WP_Query to perform a query.

WordPress itself, when you try to query a taxonomy that does not exists, adds AND (0 = 1) to the WHERE clause of the SQL query, to ensure it returns no results very quickly…

There’s a trac ticket with a patch that will probably lands in core with WP 4.6, that introduces a new filter: 'posts_pre_query'. Returning an array on that filter will make WP_Query stop processing and use the array provided as its posts array.

This could somehow helps you in implementing what you are trying to do.

Waiting fot this, anything you could do is somehow hackish, the trick core itself uses is quite hackish as well.

Recently, I’m starting using a trick when I want to stop WordPress to do things that I can’t stop in a clean way: I throw an exception and catch it to continue application flow.

I’ll show you an example. Note all the code here is completely untested.

First of all, let’s write a custom exception:

class My_StopWpQueryException extends Exception {

   private $query;

   public static forQuery(WP_Query $query) {
     $instance = new static();
     $instance->query = $query;

     return $instance;
   }

   public function wpQuery() {
     return $this->query;
   }
}

The exception is designed to act as a sort of DTO to transport a query object, so that in a catch block you can get and use it.

Better explained with code:

function maybe_cached_query(WP_Query $query) {
    $cached_query = wp_cache_get($query->query_vars_hash, 'globals');
    if ($cached_query instanceof WP_Query)
       throw My_StopWpQueryException::forQuery($cached_query);
}

function cached_query_set(WP_Query $query) {
    $GLOBALS['wp_query'] = $query;
    $GLOBALS['wp_the_query'] = $query;
    // maybe some more fine-tuning here...
}

add_action('pre_get_posts', function(WP_Query $query) {
    if ($query->is_main_query() && ! is_admin()) {
        try {
           maybe_cached_query($query);
        } catch(My_StopWpQueryException $e) {
           cached_query_set($e->wpQuery());
        }
    }
});

This should more or less work, however, there are a lot of hooks that you are not going to fire, for example "the_posts" and much more… if you have code that use one of those hooks to trigger in, it will break.

You can use the cached_query_set function to fire some of the hooks that your theme / plugins may require.

Method 2

This is PHP question more than a WordPress question.

As @Mark commented:

returning from the action do not return by magic from the calling function

That is true. Placing return in function mean exit the function and placing return in a PHP file mean exit the file. Do not get confused with PHP construct exit() 😛 (You might find a better answer on SO about PHP return).

And to answer your question

You can reduce the load of query by fetching a single column instead of full table. Like @birgire did here Remove the Homepage Query

May be a better answer yet to come. I just shared that what I know 🙂

Method 3

It will be made possible in 4.6 (assuming no changes till release) with the new posts_pre_query filter https://core.trac.wordpress.org/ticket/36687

Method 4

Yep it’s possible depending on what you want to cache. I’ve done a similar thing to cache the main loop on our homepage. Essentially you can use the posts_request and posts_results to hijack the query and hit the cache instead then also use found_posts to correct the pagination.

Really rough example pulled from our code (untested) but you should help you get the idea:

<?php
/**
 * Kill the query if we have the result in the cache
 * @var [type]
 */
add_filter( 'posts_request', function( $request, $query ) {
    if ( is_home() && $query->is_main_query() ) {

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

        $key = 'homepage_query_cache_' . $page;

        if ( wp_cache_get( $key, 'cache_group' ) )
            $request = null;

    }

    return $request;
}, 10, 2 );

/**
 * Get the result from the cache and set it as the query result
 * Or add the query result to the cache if it's not there
 * @var [type]
 */
add_filter( 'posts_results', function( $posts, $query ) {

    if ( is_home() && $query->is_main_query() ) {

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

        $key = 'homepage_query_cache_' . $page;

        if ( $cached_posts = wp_cache_get( $key, 'cache_group' ) ) {
            $posts = $cached_posts;
        } else {
            wp_cache_set( $key . '_found_posts', $query->found_posts, 'cache_group', HOUR_IN_SECONDS );
            wp_cache_set( $key, $posts, 'cache_group', HOUR_IN_SECONDS );
        }
    }

    return $posts;

}, 10, 2 );

/**
 * Correct the found posts number if we've hijacked the query results
 * @var [type]
 */
add_filter( 'found_posts', function( $num, $query ) {
    if ( is_home() && $query->is_main_query() ) {
        $page = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;

        $key = 'homepage_query_cache_' . $page;

        if ( $found_posts = wp_cache_get( $key . '_found_posts', 'cache_group' ) )
            $num = $found_posts;
    }

    return $num;
}, 10, 2 );

More here: https://www.reddit.com/r/Wordpress/comments/19crcn/best_practice_for_hijacking_main_loop_and_caching/


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Comment