10

I am trying to order the posts in a category by showing the posts with images first and then posts without images last. I have managed to do that by running two queries and now I want to merge the two queries together.

I have the following:

<?php
$loop = new WP_Query( array('meta_key' => '_thumbnail_id', 'cat' => 1 ) );
$loop2 = new WP_Query( array('meta_key' => '', 'cat' => 1 ) );
$mergedloops = array_merge($loop, $loop2);

while($mergedloops->have_posts()): $mergedloops->the_post(); ?>

But when I try and view the page then I get the following error:

 Fatal error: Call to a member function have_posts() on a non-object in...

I then tried casting array_merge to an object, but I got the following error:

Fatal error: Call to undefined method stdClass::have_posts() in...

How can I fix this error?

8

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.

6

While this is not the best way to solve this problem (@kaiser's answer is), to answer the question directly, the actual query results will be in $loop->posts and $loop2->posts, so ...

$mergedloops = array_merge($loop->posts, $loop2->posts);

... should work, but you'd need to use a foreach loop and not the WP_Query based standard loop structure as merging queries like that will break the WP_Query object "meta" data about the loop.

You can also do this:

$loop = new WP_Query( array('fields' => 'ids','meta_key' => '_thumbnail_id', 'cat' => 1 ) );
$loop2 = new WP_Query( array('fields' => 'ids','meta_key' => '', 'cat' => 1 ) );
$ids = array_merge($loop->posts, $loop2->posts);
$merged = new WP_Query(array('post__in' => $ids,'orderby' => 'post__in'));

Of course, those solutions represent multiple queries, which is why @Kaiser's is the better approach for cases like this where WP_Query can handle the logic necessary.

3

Actually there's meta_query (or WP_Meta_Query) - which takes an array of arrays - where you can search for the _thumbnail_id rows. If you then check for EXISTS, you're able to obtain only those which have this field. Combining this with the cat argument, you'll only get posts that are assigned to the category with the ID of 1 and that have a thumbnail attached. If you then order them by the meta_value_num, then you'll actually order them by the thumbnail ID lowest to highest (as stated with order and ASC). You don't have to specify the value when you use EXISTS as compare value.

$thumbsUp = new WP_Query( array( 
    'cat'        => 1,
    'meta_query' => array( 
        array(
            'key'     => '_thumbnail_id',
            'compare' => 'EXISTS',
        ),
    ),
    'orderby'    => 'meta_value_num',
    'order'      => 'ASC',
) );

Now when looping trough them, you can collect all the IDs and use them in an exclusive statement for the subsidiary query:

$postsWithThumbnails = array();
if ( $thumbsUp->have_posts() )
{
    while ( $thumbsUp->have_posts() )
    {
        $thumbsUp->the_post();

        // collect them
        $postsWithThumbnails[] = get_the_ID();

        // do display/rendering stuff here
    }
}

Now you can add your second query. No need for wp_reset_postdata() here - everything's in the variable and not the main query.

$noThumbnails = new WP_Query( array(
    'cat'          => 1,
    'post__not_in' => $postsWithThumbnails
) );
// Loop through this posts

Of course you can be much smarter and simply alter the SQL statement inside pre_get_posts to not waste the main query. You could as well simply do the first query ($thumbsUp above) inside a pre_get_posts filter callback.

add_filter( 'pre_get_posts', 'wpse130009excludeThumbsPosts' );
function wpse130009excludeThumbsPosts( $query )
{
    if ( $query->is_admin() )
        return $query;

    if ( ! $query->is_main_query() )
        return $query;

    if ( 'post' !== $query->get( 'post_type' ) )
        return $query;

    // Only needed if this query is for the category archive for cat 1
    if (
        $query->is_archive() 
        AND ! $query->is_category( 1 )
    )
        return $query;

    $query->set( 'meta_query', array( 
        array(
            'key'     => '_thumbnail_id',
            'compare' => 'EXISTS',
        ),
    ) );
    $query->set( 'orderby', 'meta_value_num' );

    // In case we're not on the cat = 1 category archive page, we need the following:
    $query->set( 'category__in', 1 );

    return $query;
}

This altered the main query, so we'll only get posts that have a thumbnail attached. Now we can (as shown in the 1st query above) collect the IDs during the main loop and then add a second query that displays the rest of the posts (without a thumbnail).

Aside from that you can get even smarter and alter posts_clauses and modify the query directly order by the meta value. Take a look at this answer as the current one is just a starting point.

3

What you need is actually a third query to get all the posts at once. Then you change your first two queries to not return the posts, but only the post IDs in a format that you can work with.

The 'fields'=>'ids' parameter will make a query actually return an array of matching post ID numbers. But we don't want the whole query object, so we use get_posts for these instead.

First, get the post IDs we need:

$imageposts = get_posts( array('fields'=>'ids', 'meta_key' => '_thumbnail_id', 'cat' => 1 ) );
$nonimageposts = get_posts( array('fields'=>'ids', 'meta_key' => '', 'cat' => 1 ) );

$imageposts and $nonimageposts will now both be an array of post ID numbers, so we merge them

$mypostids = array_merge( $imageposts, $nonimageposts );

Eliminate the duplicated ID numbers...

$mypostids = array_unique( $mypostids );

Now, make a query to get the actual posts in the order specified:

$loop = new WP_Query( array('post__in' => $mypostids, 'ignore_sticky_posts' => true, 'orderby' => 'post__in' ) );

The $loop variable is now a WP_Query object with your posts in it.

  • Thanks for this. Found this to be the least complicated solution for keeping one loop structure and uncomplicated pagination calculations. – Jay Neely Mar 7 '16 at 17:46

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.