Cache posts in category/tag/taxonomy for better performing content blocks

If you ever needed to grab a bunch of posts from different categories or taxonomies into your theme to fill certain layout blocks with their respective content you’d likely ended up doing a bunch of WP_Query calls with the desired parameters. That’s all nice and works ok, but it causes some additional database queries for the visitors which can be avoided by caching the results for these content blocks in a transient or object cache. Even better, as you know exactly that you need to update the caches only when there are changes to the terms you can even prime the cache objects from wp-admin whenever a term that matches your conditions is assigned to a post.

Here is how it could be done:

class Cached_Category_Posts { 

	private $args = array();
	private $cache_key = array();
	private $default_filter = 'cached_category_posts_defaults';
	private $args_filter = 'cached_category_posts_arguments';
	private $check_taxonomies = array( 'category' );
	
	public function __construct( $args=array() ) {
		$defaults = apply_filters( $this->default_filter, array( 'posts_per_page' => 10, 'orderby' => 'date', 'post_status' => 'publish' ) );
		$this->args = apply_filters( $this->args_filter, wp_parse_args( $args, $defaults ) );

		if ( !isset( $this->args['cat'] ) || empty( $this->args['cat'] ) )
			return new WP_Error( 'cached_category_posts_error', __( 'cat argument cannot be empty' ) );

		// make sure $this->args['cat'] is an integer
		$this->args['cat'] = (int) $this->args['cat'];

		// build a cache key based on the arguments
		$this->cache_key = md5( serialize( $this->args ) );

		// we know we only need to update the cache when terms change, so lets monitor term changes
		add_action( 'set_object_terms', array( &$this, 'maybe_refresh_cache' ), 9999, 6 );
	}

	/** 
	 * Cause a cache update whenever a term changes. Make sure to do this only when also other argument parameters
	 * fit the post data to avoid cache refreshs every time a draft is saved
	 */
	public function maybe_refresh_cache( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
		if ( !in_array( $taxonomy, $this->check_taxonomies ) )
			return;

		if ( !isset( $this->args['cat'] ) || empty( $this->args['cat'] ) )
			return;

		$post = get_post( $object_id );
		if ( is_wp_error( $post ) )
			return;

		// compare additional values in the post with the arguments set. eg post_status = publish
		foreach( array_keys( (array) $post ) as $key ) {
			if ( isset( $this->args[$key] ) && $post->{$key} <> $this->args[$key] ) {
				return;
			}
		}

		// grab an array of all terms included in this post and receive their term_ids
		$all_terms = array_unique( array_merge( $tt_ids, $old_tt_ids ) );
		$all_terms = array_map( array( &$this, 'get_term_id_from_tt_id' ), $all_terms );
		
		// if none of the set categories is in our arguments just skip it
		if ( !in_array( $this->args['cat'], $all_terms ) )
			return;
		
		// otherwise we need to refresh the cache
		$this->update();
	}

	/**
	 * Receive the term_id from a term_taxonomy_id
	 */
	private function get_term_id_from_tt_id( $tt_id ) {
		global $wpdb;
		return $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $tt_id ) );
	}

	/**
	 * Refresh the caches by calling get() with force_refresh = true
	 */
	public function update() {
		return $this->get( $force_refresh = true );
	}

	/**
	 * Run a WP_Query call with a check against a cached result first.
	 */
	public function get( $force_refresh = false ) {
		if ( !isset( $this->args['cat'] ) || empty( $this->args['cat'] ) )
			return new WP_Error( 'cached_category_posts_error', __( 'cat argument cannot be empty' ) );;
		
		$result = get_transient( $this->cache_key );
		if ( ! $result || true === $force_refresh ) {
			$result = new WP_Query( $this->args );
			set_transient( $this->cache_key, $result );
		}
		return $result;
	}

}

In your themes’ functions.php you would then initialize the class and the different cached query objects like this:

// initialize all the category caches we need.
add_action( 'init', 'my_setup_category_caches' );
function my_setup_category_caches() {
	global $my_cat_stories;
	$my_cat_stories['home-top-story'] = new Cached_Category_Posts( $args = array( 'cat' => 1, 'posts_per_page' => 1 ) );
	// possible usage in theme template $topstories = my_get_category_posts( 'home-top-story-2' );
}

// get category posts by their category identifier
function my_get_category_posts( $id ) {
	global $my_cat_stories;
	if ( isset( $my_cat_stories[$id] ) )
		return $my_cat_stories[$id]->get();
	else
		return false;
}

Although this should work nice you might want to keep in mind that this would store the whole WP_Query object in the transient. It makes most sense to utilize this only for small content blocks and not for some loops with a high amount of posts.

No comments yet... Be the first to leave a reply!

Leave a comment