You Don't Know Query - WordCamp Portland 2011

Preview:

DESCRIPTION

The slides for my talk, You Don't Know Query, at WordCamp Portland on September 17, 2011.

Citation preview

WordCamp Portland 2011 September 17, 2011

Andrew Nacin Core Developer of WordPress Tech Ninja at Audrey Capital

nacin@wordpress.org @nacin on Twitter

You Don't Know Query

What do you know?

Conditional Tags

is_author( ), is_home( ), etc.

Who has ever heard of query_posts( )?

Ways to query

query_posts( ) new WP_Query( ) get_posts( )

The loop

if ( have_posts( ) ) while ( have_posts( ) ) : the_post( );

endwhile( );

What don't you know?

Every query object has its own methods

is_author( ) is the same as calling $wp_query->is_author( )

function is_author( ) { global $wp_query;

return $wp_query->is_author( );

}

If you do: $my_query = new WP_Query( $query ); You can do: while ( $my_query->have_posts( ) ) : $my_query->the_post( ); endwhile; wp_reset_postdata( );

But why do we call things like wp_reset_postdata( ) and wp_reset_query( )? What about using query_posts( )? How can you alter a query? What about the main query?

What is the main query, and why should I care?

Let's dig in.

wp-blog-header.php // Load the WordPress bootstrap require dirname( __FILE__ ) . '/wp-load.php'; // Do magic wp(); // Decide which template files to load ABSPATH . WPINC . '/template-loader.php';

Let's look in the bootstrap: $wp_the_query = new WP_Query(); $wp_query =& $wp_the_query;

Quick lesson on PHP references

$a = 4; $b =& $a; $b = 2; var_dump( $a ); // int(2) $a = 6; var_dump( $b ); // int(6)

So: So the real main query is in $wp_the_query. And a live copy of it is stored in $wp_query.

wp-blog-header.php // Load the WordPress bootstrap require dirname( __FILE__ ) . '/wp-load.php'; // Do magic wp(); // Decide which template files to load ABSPATH . WPINC . '/template-loader.php';

wp-blog-header.php // Load the WordPress bootstrap require dirname( __FILE__ ) . '/wp-load.php'; // Do magic wp( ); // Decide which template files to load ABSPATH . WPINC . '/template-loader.php';

What is that wp( ) call?

function wp( $query_vars = '' ) { global $wp;

$wp->main( $query_vars );

}

Holy $!@?, what just happened?

In the bootstrap:

$wp = new WP( ); So there's a wp( ) function, and a WP class.

class WP { . . . function main( ) { $this->init( ); $this->parse_request( ); $this->send_headers( ); $this->query_posts( ); $this->handle_404( ); $this->register_globals( ); . . .

class WP { . . . function main( ) { $this->init( ); $this->parse_request( ); $this->send_headers( ); $this->query_posts( ); $this->handle_404( ); $this->register_globals( ); . . .

WP::parse_request( ) — Parses the URL using WP_Rewrite — Sets up query variables for WP_Query WP::query_posts( ) {

global $wp_the_query; $wp_the_query->query( $this->query_vars );

}

Boom. SELECT SQL_CALC_FOUND_ROWS

wp_posts.* FROM wp_posts WHERE 1=1

AND wp_posts.post_type = 'post' AND wp_posts.post_status = 'publish'

ORDER BY wp_posts.post_date DESC LIMIT 0, 10

wp-blog-header.php

// Load WordPress. require dirname(__FILE__) . '/wp-load.php'; // Parse what to query, and query it. wp(); // Load the theme. ABSPATH . WPINC . '/template-loader.php';

Before we get to the theme, we have your posts.

Got it?

Then why do we do this?

query_posts( 'author=5' ); get_header( ); while( have_posts( ) ) : the_post( ); endwhile; get_footer( );

That's running 2* queries! One, the query WordPress thought we wanted. Two, this new one you're actually going to use.

* Actually, WP_Query doesn't run just one query. It usually runs four.

1. Get me my posts: SELECT SQL_CALC_FOUND_ROWS … FROM wp_posts LIMIT 0, 10

2. How many posts exist? SELECT FOUND_ROWS()

3. Slurp all metadata for these posts. 4. Slurp all terms for these posts.

PROTIP ‘Measure twice, cut once’ is bad for performance.

(A note, you can turn these off selectively…)

$my_query = new WP_Query( array( 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false,

) );

So. Instead of this:

query_posts( 'author=5' ); get_header( ); while ( have_posts( ) ) : the_post( ); endwhile; get_footer( );

We can use this:

// In WP::parse_request() $this->query_vars =

apply_filters( 'request', $this->query_vars );

We can modify query variables in mid air:

function nacin_filter_out_author( $qvs ) { if ( ! isset( $qvs['author'] ) ) $qvs['author'] = '-5'; return $qvs;

}

Powerful, but lacks context.

Powerful, but lacks context.

Problem 1: Conditional tags don't work yet.

Powerful, but lacks context.

Problem 1: Conditional tags don't work yet. Problem 2: Only works on the main query.

Powerful, but lacks context.

Problem 1: Conditional tags don't work yet. Problem 2: Only works on the main query. Problem 3: WP_Query is waaay cooler.

Introducing pre_get_posts class WP_Query {

. . . function &get_posts() { $this->parse_query(); // Huzzah! Conditional tags are available. do_action_ref_array( 'pre_get_posts', array( &$this ) ); . . .

A truly awesome hook. function nacin_alter_home( $query ) {

if ( $query->is_home( ) ) $query->set( 'author', '-5' );

} add_action( 'pre_get_posts', 'nacin_alter_home' );

Still with us?

Good, ‘cause here’s where things get hairy.

'request' fires for the main query only. 'pre_get_posts' fires for every post query: — get_posts() — new WP_Query() — That random recent posts widget. — Everything.

What if I just want it on the main query?

$wp_the_query makes a triumphant return.

Main query only!

function nacin_alter_home ( $query ) { if ( $wp_the_query === $query && $query->is_home() ) $query->set( 'author', '-5' );

} add_action( 'pre_get_posts', 'nacin_alter_home' );

Hmm. How does this work? $wp_the_query should never be modified. It holds the main query, forever. $wp_query keeps a live reference to $wp_the_query, unless you use query_posts().

query_posts( 'author=-5' ); while ( have_posts( ) ) :

the_post( ); endwhile; wp_reset_query( );

query_posts( 'author=-5' ); while ( have_posts( ) ) :

the_post( ); endwhile; wp_reset_query( );

class WP_Query { . . . function &query_posts( $query ) { // Break the reference to $wp_the_query unset( $wp_query ); $wp_query =& new WP_Query( $query ); . . .

query_posts( 'author=-5' ); while ( have_posts( ) ) :

the_post( ); endwhile; wp_reset_query( );

class WP_Query { . . . function wp_reset_query( ) { // Restore the reference to

$wp_the_query unset( $wp_query ); $wp_query =& $wp_the_query;

// Reset the globals, too. wp_reset_postdata( ); . . .

Calling the_post( )? wp_reset_query( ) will reset $wp_query and and the globals.

Calling $my_query->the_post( )?

wp_reset_postdata( ) will reset the globals.

New thing for core in 3.3!

Rather than: $wp_the_query === $other_query_object

 You'll be able to call:

$other_query_object->is_main_query( )  is_main_query( ), the function, will act on $wp_query, like any other conditional tag.

Some Lessons

Every WP_Query object has methods that mimic the global conditional tags. The global conditional tags apply to $wp_query, the main or current query. $wp_query is always the main query, unless you use query_posts( ). Restore it with wp_reset_query( ).

request is a nice hook. pre_get_posts is more powerful and flexible. Just use it properly. Always check if you're modifying the main query using $query === $wp_the_query $query->is_main_query( ) in 3.3!

And Finally

Thanks! Questions?

@nacin