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?
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 setposts_per_page
andcat
. Example without thepre_get_posts
-Filter: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):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:accept()
method allows to define criteria that allow looping the item – or not.WP_Query::the_post()
, so you can simply use every template tag in your template files loop.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() )
andwhile ( $customQuery->have_posts() )
anymore. We can go with a simpleforeach
statement as all needed checks are already done for us. Example:Finally we need nothing more than a default
foreach
loop. We can even dropthe_post()
and still use all template tags. The global$post
object will always stay in sync.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.As we defined that we now
deny()
looping every post that has a thumbnail, we then can instantly loop all posts without a thumbnail: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 thevar_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.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 …… should work, but you’d need to use a
foreach
loop and not theWP_Query
based standard loop structure as merging queries like that will break theWP_Query
object “meta” data about the loop.You can also do this:
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.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 and $nonimageposts will now both be an array of post ID numbers, so we merge them
Eliminate the duplicated ID numbers…
Now, make a query to get the actual posts in the order specified:
The $loop variable is now a WP_Query object with your posts in it.
Actually there’s
meta_query
(orWP_Meta_Query
) – which takes an array of arrays – where you can search for the_thumbnail_id
rows. If you then check forEXISTS
, you’re able to obtain only those which have this field. Combining this with thecat
argument, you’ll only get posts that are assigned to the category with the ID of1
and that have a thumbnail attached. If you then order them by themeta_value_num
, then you’ll actually order them by the thumbnail ID lowest to highest (as stated withorder
andASC
). You don’t have to specify thevalue
when you useEXISTS
ascompare
value.Now when looping trough them, you can collect all the IDs and use them in an exclusive statement for the subsidiary query:
Now you can add your second query. No need for
wp_reset_postdata()
here – everything’s in the variable and not the main query.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 apre_get_posts
filter callback.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.