advanced post queries
DESCRIPTION
Whether you’re maintaining a personal blog, or leveraging WordPress as your CMS backbone, you will eventually want to modify what content WordPress sends to your pages from the database. WordPress itself provides a rich set of tools and methods for interacting with the database, most of which don’t even require database expertise. Each method is appropriate in certain circumstances, and should be avoided in others. This talk discusses each method of generating or modifying the database query when and how each should be used. While this discussion focuses on coding, even those just starting out will be sure to learn best practices around how and when to use “query_posts()” vs. “get_posts()” vs. “new WP_Query()” to build custom lists of posts. Seasoned developers will appreciate discovering the right way to use hooks like “pre_get_posts”, “request” and “posts_clauses” to access the query at the lowest levels.TRANSCRIPT
Advanced Post QueriesBest Practices for Accessing your Data
@TomAugerWordCamp Montreal 2012
What's in the database?*
• Posts• Pages• Attachment records• Revisions• Nav Menus• Custom Post Types• Post Meta• Categories• Tags• Custom Taxonomies
• Users• User meta• Site Options
– Your URL– Widget settings– Theme options– User capabilities (maybe)
• * basically, everything
Post Queries in the URL(look Ma, no code!)
• Settings > Reading– "Your Latest Posts"– Front page– Posts page
• Page / post slug• Category Archive• Date• Author Archive• post_type• Search
– / (index.php)– / (front-
page.php)– /posts-page-slug/
• /post-or-page-slug/• /category/term/• /year/[month/]• /author/author-nicename/• /post-type-slug/• /search/term/
Post Queries (code)
• query_posts( $args )• new WP_Query( $args )• get_posts( $args )• get_pages( $args )
And where does it go?
• Most often on the PHP page template• Widgets defined in plugins• Hooks declared in functions.php or
include
query_posts() usageOld Skool
• Looks like URL query parameters• Mimics the browser address, so may feel more
familiar• Kinda hard to read…
query_posts( ‘cat=6&post_type=product&numberposts=5’ );
query_posts() usageThe Cool Way…
• Supply arguments as array of key => value pairs• Lots more brackets and stuff, but…• Kinda looks cool once you get used to it
query_posts( array(‘cat’ => 6,‘post_type’ => ‘product’,‘posts_per_page’ => 5
) );
What can we query?
• author• category / tag / tax• post name, ID(s)• page parent• post type• post status• date & time
• custom fields (meta)
• sticky• add pagination• set sort order and
criteria• comment count• even random
posts!
http://codex.wordpress.org/Class_Reference/WP_Query
query_posts ExampleMovie of the Day!
• Create a new Page• Only the slug is important• Add query_posts() before any HTML• Current page content is clobbered
• http://localhost/projects/wordcamp/query_posts-example/
query_posts( array ('orderby' => 'rand','posts_per_page' => 1
) );
query_posts to Limit Page Count?
• Common example is to change # posts displayed
• Loads the posts twice*!• Consider Settings > Reading > Blog
Pages Show At Most…
• * and then some…
query_posts( ‘posts_per_page=20’ );
What’s up with query_posts()?
• http://codex.wordpress.org/Function_Reference/query_posts
• Only useful for altering the Main Loop• Can break pagination– (but can be fixed with effort)
• Still executes the original query!– (and then some)
• Don't use in a subquery!
Why Subqueries?
• Add related posts• Add child pages• Add other types of content (eg:
News)• Widgets!
Is there a template tag for that?
• http://codex.wordpress.org/Template_Tags
• Author info (the_author(), the_author_meta() )
• Metadata (the_meta() )• Categories, tags• Navigation (eg: next_post_link() )
Template Tag Example
• http://localhost/projects/wordcamp/template-tags/
• Inside "The Loop", powered by the_post()
<div class="post-meta"><?php the_meta() ?></div><div class="categories"><?php the_category(); ?></div><div class="tags"><?php the_tags(); ?></div>
<div class="nav"><?php previous_post_link() ?>|<?php next_post_link() ?>
</div>
"Real" Sub-queries: WP_Query
• The brains behind all queries on the wp_posts table
• Defined in wp-includes/query.php• All queries go through WP_Query• "Get your own" with
new WP_Query( $args );
WP_Query usage
<?php $my_sub_query = new WP_Query( array ( 'post_type' => 'quotes', 'order' => 'rand', 'posts_per_page' => 1 ) );
while ( $my_sub_query->have_posts() ){ $my_sub_query->the_post(); get_template_part( 'my_loop_template' ); }
wp_reset_postdata();}
WP_Query Examples
• http://localhost/projects/wordcamp/date-query/
• http://localhost/projects/wordcamp/sort-by-post-meta/
Meta Queries
• Make your postmeta work (harder) for you!
• At first blush, a little funky
<?php $meta_query = new WP_Query( array( 'meta_query' => array( array( // meta query here ) ) ) );
More Meta Query Examples
• http://localhost/projects/wordcamp/meta-queries/
• http://localhost/projects/wordcamp/multiple-meta-queries-and/
• http://localhost/projects/wordcamp/multiple-meta-queries-or/
Taxonomy Queries
• As with meta_query, so with tax_query
• http://localhost/projects/wordcamp/tax-query-simple/
<?php $tax_query = new WP_Query( array( 'taz_query' => array( array( ) ) ) );
wp_reset_postdata()?
• If you use the_post(), clean up after yourself
• the_post() sets lots of $_GLOBALs• Could break templates, footers,
widgets etc…
• http://localhost/projects/wordcamp/wp_reset_postdata/
Global Query Manipulation(The Good Stuff!)
• Modify the Main Query• Modify the way all queries work site-
wide• Modify queries for a specific page
Case Study: amovo.ca• Furniture retailer with multiple
brands• Custom post type: "amovo-products"• Custom post type: "manufacturer"• Custom taxonomies: "product-
categories" and "product-applications"
• http://localhost/projects/amovo/
amovo.ca: Custom Global Sort
• Wanted to "feature" certain products• Wherever they might be listed• eg:
http://localhost/projects/amovo/product-categories/executive-conference-seating/
• Best match: custom field "_zg_priority"
amovo.ca: Category Archive
• Uses the standard custom tax query• Need to ORDER BY wp_postmeta for
ALL queries
Filter: 'posts_clauses'
• Allows us to manipulate SQL before it is sent to the database
• For ALL queries (not just main query)if (! is_admin()){ add_filter( 'posts_clauses', 'sort_by_priority', 10, 2 );}
function sort_by_product_priority( $clauses, $query ){ global $wpdb;
//… more to come}
Keeping it lean• If query has nothing to do with
products, don’t add the overhead• Only affect product-related tax
queries
function sort_by_product_priority( $clauses, $query ){ global $wpdb;
if ( in_array( $query->query_vars['taxonomy'], array( 'product-category', 'product-application')) ){ // … }}
Make sure it's the right query!
• These hooks affect EVERY query• Add conditionals to reduce overhead– is_admin() during hook registration– is_main_query()– $query->query_vars (eg: "taxonomy",
"post_type", "orderby")
if ( 'page-hijacking' == $query_obj->query_vars['pagename'] ){ … }
JOIN your tables• LEFT JOIN doesn't exclude missing
meta• Alias your JOINs to avoid collision
function sort_by_product_priority( $clauses, $query ){ global $wpdb; … // JOIN on postmeta to get priority $clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} PM ON ({$wpdb->posts}.ID = PM.post_id AND PM.meta_key = '_zg_priority') "; … return $clauses;}
Modify ORDER BY• PREPEND your sort criteria• Don't forget to add comma if you
need one!function sort_by_product_priority( $clauses, $query ){ … $join .= "LEFT JOIN {$wpdb->postmeta} PM..."; … $orderby = &$clauses['orderby']; // by reference! if (! empty( $orderby ) ) $orderby = ', ' . $orderby;
$orderby = " IFNULL(P.meta_value, 5) ASC, {$wpdb->posts}.post_title ASC" . $orderby;
return $clauses;}
Manipulating the Main Query(main filters)
• Manipulating the URI query variables– before the query is parsed– add_filter( 'request' )
• Manipulating the query object– before the SQL is generated– add_filter( 'parse_query' ) / 'pre_get_posts'
• Manipulating the SQL– before the SQL is run– add_filter( 'posts_clauses' ) 2 arguments
• Manipulating the results– before stickies and status– add_filter( 'posts_results' )
Case Study: Date Range
• WP_Query doesn't (currently) support date ranges
• Let's create 'before_date' and 'after_date'
$date_query = new WP_Query( array('after_date' => '2012-08-10','before_date' => '2012-08-14'
) );
Exploiting the Query Object
add_filter( 'posts_where', 'date_ranges', 10, 2 );function 'date_ranges( $where_clause, $query_obj ){ global $wpdb;
if ( $before_date = $query_obj->query['before_date'] ){ $where_clause .= " AND {$wpdb->posts}.post_date < '{$before_date}' "; }
if ( $after_date = $query_obj->query['after_date'] ) ){ $where_clause .= " AND DATE({$wpdb->posts}.post_date) > '{$after_date}' "; }
return $where_clause;}
Improvements & Security• Make sure we're actually getting a
Date• Cast the time() object to a mySQL
DATE string• Cast mySQL TIMESTAMP to DATE
portion onlyif ( $after_date = strtotime( $query_obj->query['after_date'] ) ){ $after_date = date( 'Y-m-d', $after_date ); $where_clause .= " AND DATE({$wpdb->posts}.post_date) > '{$after_date}' ";}
Conclusions
• TMTOWTDI• Go as high-level as you can• Use query_posts() sparingly• wp_reset_postdata()!• Modify the Main Query with filters /
actions
• Make WordPress sing!
Working with $wpdb
• global $wpdb• Always use $wpdb->table_name• Always use $wpdb->prepare with
tainted data• Use the right get_* method for the job– get_var()– get_row()– get_col()– get_results()
Please avoid inserting / updating…
• Posts• Users• Meta data (usermeta or postmeta)• Options• Taxonomies and Terms (especially!)
• Basically, if there's a default wp_* table, there are core methods for inserting, updating and deleting
When to extend schema
• Primary object has tons of metadata• Highly relational data• Plugin tables (be sure to offer DROP
option!)– register_deactivation_hook();
• Can't map to post, meta and taxonomy
Final observations… 'n stuff
• There are actually very few reasons to generate your own SQL
• There's probably a core function for that
• Due diligence and research– Google search "wordpress [keywords]"!– Get a good IDE (Jer's talk about
NetBeans!)
Thank You!updates: @TomAugerwww.tomauger.com
Extra Slides
• Ah, time is thy enemy…
More Template Tags
<div class="author-info"> <p><strong>Posted by: </strong> <?php the_author(); ?> (<?php the_author_meta( 'first_name' ) ?> <?php the_author_meta( 'last_name' ) ?>)
<?php echo apply_filters( 'the_content', get_the_author_meta( 'description' ) ); ?></div>
Anatomy of a Query• $wp->main() is called after everything is loaded
– Parses the browser query string and applies rewrite rules– Rebuilds the query string for main query– Runs get_posts() on the main $wp_the_query (WP_Query instance)
• WP_Query then– Reparses the query string
• Request for single page short-circuits and uses cache if possible
– Parses the search– Parses any taxonomy queries– Generates the SQL clauses– Gives cacheing plugins a chance to get involved– Generates the SQL request and executes it!– Processes comments– Processes stickies– Updates caches
Plugin Authors
• Consider require_once( wp-admin/install-helper.php ) within your register_activation_hook()– helper functions like
maybe_create_table(), maybe_add_column()