Skip to content

Instantly share code, notes, and snippets.

@franz-josef-kaiser
Last active January 18, 2021 19:15
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save franz-josef-kaiser/8464919 to your computer and use it in GitHub Desktop.
Save franz-josef-kaiser/8464919 to your computer and use it in GitHub Desktop.
An example plugin to show the use of the PHP SPL and subsidiary loops with a FilterIterator.
<?php
// Reduced to the minimum
class ThumbnailFilter extends FilterIterator
{
private $wp_query;
public function __construct( Iterator $iterator, WP_Query $wp_query )
{
NULL === $this->wp_query AND $this->wp_query = $wp_query;
parent::__construct( $iterator );
}
public function accept()
{
$this->wp_query->the_post();
$this->wp_query->current_post === $this->wp_query->query_vars['posts_per_page'] // -1
AND $this->wp_query->rewind_posts();
return $this->wp_query->have_posts() AND $this->deny();
}
public function deny()
{
return ! has_post_thumbnail( $this->current()->ID );
}
}
<?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;
}
}
class NoThumbnailFilter extends ThumbnailFilter
{
public function deny()
{
return has_post_thumbnail( $this->current()->ID );
}
}
add_action( 'loop_start', 'wpse130009Query' );
function wpse130009Query()
{
// Only need to remove this callback for the current test
// Else we'd infinitely nest this callback as WP_Query::the_post()
// calls the 'loop_start' filter.
remove_action( current_filter(), __FUNCTION__ );
global $wp_query;
echo number_format_i18n( $wp_query->found_posts );
$arrayObj = new ArrayObject( $wp_query->get_posts() );
$primaryQuery = new ThumbnailFilter( $arrayObj->getIterator(), $wp_query );
$secondaryQuery = new NoThumbnailFilter( $arrayObj->getIterator(), $wp_query );
foreach ( $primaryQuery as $post )
{
var_dump( get_the_ID() );
}
foreach ( $secondaryQuery as $post )
{
var_dump( get_the_ID() );
}
}
@samjco
Copy link

samjco commented Jan 16, 2021

so how do we use this script in a real world example?

@franz-josef-kaiser
Copy link
Author

franz-josef-kaiser commented Jan 16, 2021

@samjco The full text explanation can be found in my two answers on WordPress StackExchange here.

You are looking at a WordPress plugin above. If you want to give it a test run, just copy both files into a folder inside wp-content/plugins and activate the plugin in the WP admin areas plugin list screen. It will print two lists of post IDs at the beginning of every loop: One for posts that have thumbnails, another one for posts without thumbnails.

@samjco
Copy link

samjco commented Jan 16, 2021

Yep.

I am still trying to understand the concept a bit..
how can I take a regular args like:

$args = array(
    'date_query' => array( array( 'after' => '1 week ago' ) ),  
    'posts_per_page' => 99,
    'ignore_sticky_posts' => 1,
    'order' => 'DESC',
    'cat' => '-907,-908,-909'
);

And use it with your function...even 2 or more args?

@franz-josef-kaiser
Copy link
Author

The query itself has nothing to do with above filt
er iterators.

  1. Option, The Main Query: Modify the query by using a filter (in a plugin, mu-plugin, functions.php, …). Then just add the action right before executing the loop. As you can see inside wpse130009Query(), the callback removes itself instantly. Example:
# Alter main query by using a filter, search StackExchange for a how-to.

# Inside a template:
add_action( 'loop_start', 'wpse130009Query' );
while( have_posts() ) {
    the_post();
    # …do stuff
}
  1. Option: Use a new, custom \WP_Query( …your args - see docs… ). Then use what you can read in the wpse130009Query() callback above in your loop:
$custom = new \WP_Query( …args… );
if ( $custom->have_posts() ) {
    echo number_format_i18n( $custom->found_posts );

    $arrayObj = new ArrayObject( $custom->get_posts() );
    $primaryQuery = new ThumbnailFilter( $arrayObj->getIterator(), $custom );
    // $secondaryQuery = new NoThumbnailFilter( $arrayObj->getIterator(), $custom );

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

@samjco
Copy link

samjco commented Jan 16, 2021

@franz-josef-kaiser
Thanks for the explanation...I understand 2.Option, clearly, I think.

It would be nice if you made a few real examples...

What is the diff between ThumbnailFilter and NoThumbnailFilter? Is it just an image or no image?
And why would this function be useful as opposed to doing it with get_posts and WP_query?

@franz-josef-kaiser
Copy link
Author

@samjco You can find plenty of examples on StackExchange sites and StackOverflow themselves as well as various blogs. For the reasoning, read for example this SO answer. The filters explain themselves if you look at their code above. They wrap basic WP functions that you can look up at WPs dev resources. Hope that helps.

@samjco
Copy link

samjco commented Jan 17, 2021

Ok, so the main goal is to first define one query with all args. Then from there, you can remove args using deny(), thus making the main query into 2 queries. First (the main) query results, of all desired args, and the other query results are with less-than all args as - we take things out using the function: deny()... Thus splitting the query into two.
Is this correct?

@franz-josef-kaiser
Copy link
Author

@samjco Yes, you got it. Save DB-queries and just split the the data set into multiple loops, easy and readable.

@samjco
Copy link

samjco commented Jan 17, 2021

So then the class "ThumbnailFilter" and "NoThumbnailFilter" can be named "FirstQueryResults (or MainQueryResults)" AND "SecondQueryResults"...
I will also attempt to make a real-life example for others who were stumped like me...

@samjco
Copy link

samjco commented Jan 17, 2021

Another question...
Doesn't this show each query results in one after another instead of mixed in together?
So in order words... The $primaryQuery results all shows first and then $secondaryQuery's results:

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

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

How can we mix together? For Instance. What if I want two different results mixed together forming one result that order-by "date" of each post?

@franz-josef-kaiser
Copy link
Author

I am not sure what you are up to. The WP.SE answer explains whats's happening here. If you got a different question, please ask a follow up question on WP.SE directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment