8

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?

6
  • I feel like the plugin should handle this... Are you sure you're going about this the right way? Have you asked about this on their forums? On their github issues?
    – Howdy_McGee
    Jun 14, 2016 at 17:07
  • @Howdy_McGee the plugin uses the same functionalities as default WordPress caching API. The only difference is it helps connect to Redis server. Of course, I'm also trying to find the right way.
    – wpclevel
    Jun 14, 2016 at 17:15
  • not sure why do you think that the query will not fire. returning from the action do not return by magic from the calling function Jun 14, 2016 at 17:35
  • @MarkKaplun I also double about that but return might be the only command we can call in this case.
    – wpclevel
    Jun 14, 2016 at 17:40
  • @Dan, I don't get what is that you assume, you obviously assume something which is not true, probably at the php level Jun 14, 2016 at 17:49

4 Answers 4

12

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.

9
  • Why doesn't work with default exception class ? It shows me an error of uncaught exception ?
    – Sumit
    Jun 14, 2016 at 20:07
  • It should work with standard exception and a public property, but you need to catch the standard exception if you throw it @Sumit
    – gmazzap
    Jun 14, 2016 at 20:12
  • Well I did it just by using this example. But I get uncaught exception error. I ran the simplest example of exception but seems like do_action should be in try block.
    – Sumit
    Jun 14, 2016 at 20:17
  • Interesting approach that can be applied in various places in WordPress, I will keep it in mind ;-) ps: DTO = Data Transfer Object ?
    – birgire
    Jun 15, 2016 at 7:13
  • @birgire yes :)
    – gmazzap
    Jun 15, 2016 at 9:59
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() :P (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 :)

7
  • 1
    @Dan did you get many database hits after neutralizing the query request via the posts_request filter? With that + single column approach we exit WP_Query earlier than using the posts_pre_query filter.. Also watch out for the sticky posts with posts_pre_query but we can remove it with $q->set( 'ignore_sticky_posts', 1 ); in e.g. the example here.
    – birgire
    Jun 15, 2016 at 7:00
  • @birgire Look like posts_pre_query doesn't help. Your solution is the best so far. :) If you know how we can exit query right after pre_get_posts, that could be great. Thank you!
    – wpclevel
    Jun 21, 2016 at 13:49
  • @Dan posts_pre_query will be available from 4.6 ;)
    – Sumit
    Jun 21, 2016 at 14:07
  • Another approach that comes to mind is to try extending the WP_Query class with a custom get_posts() method, with a possible early exist and that calls parent::get_posts() and try to override the relevant query with it. But I don't know if that would work or make sense with your case here ;-) @Dan
    – birgire
    Jun 21, 2016 at 14:16
  • 1
    Maybe a little bit of Aerosmith - Livin' On The Edge could help with that ;-) @Dan
    – birgire
    Jun 21, 2016 at 15:26
2

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

2
  • @Dan, this is what happens when you want to complete the thought you had before going to sleep and not reading the other answers first ;) Jun 15, 2016 at 7:02
  • Bro, it's too late now. I will read those answers later ;-)
    – wpclevel
    Jun 15, 2016 at 7:30
2

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/

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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