How to merge two queries together

A single query

Thought about this a bit more and there’s a chance that you can go with a single/the main query. Or in other words: No need for two additional queries when you can work with the default one. And in case you can’t work with a default one, you won’t need more than a single query no matter for how many loops you want to split the query.

Prerequisites

First you need to set (as shown in my other answer) the needed values inside a pre_get_posts filter. There you’ll likely set posts_per_page and cat. Example without the pre_get_posts-Filter:

$catID = 1;
$catQuery = new WP_Query( array(
    'posts_per_page' => -1,
    'cat'            => $catID,
) );
// Add a headline:
printf( '<h1>%s</h1>', number_format_i18n( $catQuery->found_posts )
    .__( " Posts filed under ", 'YourTextdomain' )
    .get_cat_name( $catID ) );

Building a base

The next thing we need is a small custom plugin (or just put it into your functions.php file if you don’t mind moving it around during updates or theme changes):

<?php
/**
 * Plugin Name: (#130009) Merge Two Queries
 * Description: "Merges" two queries by using a <code>RecursiveFilterIterator</code> to divide one main query into two queries
 * Plugin URl:  http://wordpress.stackexchange.com/questions/130009/how-to-merge-two-queries-together
 */

class ThumbnailFilter extends FilterIterator implements Countable
{
    private $wp_query;

    private $allowed;

    private $counter = 0;

    public function __construct( Iterator $iterator, WP_Query $wp_query )
    {
        NULL === $this->wp_query AND $this->wp_query = $wp_query;

        // Save some processing time by saving it once
        NULL === $this->allowed
            AND $this->allowed = $this->wp_query->have_posts();

        parent::__construct( $iterator );
    }

    public function accept()
    {
        if (
            ! $this->allowed
            OR ! $this->current() instanceof WP_Post
        )
            return FALSE;

        // Switch index, Setup post data, etc.
        $this->wp_query->the_post();

        // Last WP_Post reached: Setup WP_Query for next loop
        $this->wp_query->current_post === $this->wp_query->query_vars['posts_per_page'] -1
            AND $this->wp_query->rewind_posts();

        // Doesn't meet criteria? Abort.
        if ( $this->deny() )
            return FALSE;

        $this->counter++;
        return TRUE;
    }

    public function deny()
    {
        return ! has_post_thumbnail( $this->current()->ID );
    }

    public function count()
    {
        return $this->counter;
    }
}

This plugin does one thing: It utilizes the PHP SPL (Standard PHP Library) and its Interfaces and Iterators. What we now got is a FilterIterator that allows us to conveniently remove items from our loop. It extends the PHP SPL Filter Iterator so we don’t have to set everything. The code is well commented, but here’re some notes:

  1. The accept() method allows to define criteria that allow looping the item – or not.
  2. Inside that method we use WP_Query::the_post(), so you can simply use every template tag in your template files loop.
  3. And as well are we monitoring the loop and rewinding the posts when we reach the last item. This allows to loop trough an infinite amount of loops without resetting our query.
  4. There’s one custom method that isn’t part of the FilterIterator specs: deny(). This method is especially convenient as it contains only our “process or not”-statement and we can easily overwrite it in later classes without needing to know anything aside from WordPress template tags.

How to loop?

With this new Iterator, we don’t need if ( $customQuery->have_posts() ) and while ( $customQuery->have_posts() ) anymore. We can go with a simple foreach statement as all needed checks are already done for us. Example:

global $wp_query;
// First we need an ArrayObject made out of the actual posts
$arrayObj = new ArrayObject( $wp_query->get_posts() );
// Then we need to throw it into our new custom Filter Iterator
// We pass the $wp_query object in as second argument to keep track with it
$primaryQuery = new ThumbnailFilter( $arrayObj->getIterator(), $wp_query );

Finally we need nothing more than a default foreach loop. We can even drop the_post() and still use all template tags. The global $post object will always stay in sync.

foreach ( $primaryQuery as $post )
{
    var_dump( get_the_ID() );
}

Subsidiary loops

Now the nice thing is that every later query filter is quite easy to handle: Simply define the deny() method and you’re ready to go for your next loop. $this->current() will always point to our currently looped post.

class NoThumbnailFilter extends ThumbnailFilter
{
    public function deny()
    {
        return has_post_thumbnail( $this->current()->ID );
    }
}

As we defined that we now deny() looping every post that has a thumbnail, we then can instantly loop all posts without a thumbnail:

foreach ( $secondaryQuery as $post )
{
    var_dump( get_the_title( get_the_ID() ) );
}

Test it.

The following test plugin is available as Gist on GitHub. Simply upload and activate it. It outputs/dumps the ID of every looped post as callback on the loop_start action. This means that might get quite a bit output depending on your setup, number of posts and configuration. Please add some abort statements and alter the var_dump()s at the end to what you want to see and where you want to see it. It’s just a proof of concept.

Leave a Comment