In the first post in this series, we took defined what the API is, how it differs form the settings API, and some of the general calls that we can make to the API. In this post, we'll be taking a look at a practical implementation of the API and how to handle some idiosyncrasies that come with dealing with expired data.
The WordPress Transients API is a powerful (but really-easy-to-use) aspect of the WordPress API. Generally speaking, it makes it really easy to store data with an expiration time, and makes it really easy to take advantage of various caching plugins to ultimately increase the speed of your site.
Setup The Plugin
For the purposes of this plugin, we're going to create a simple widget that will list a blogs top commenters of all time. The goal of the plugin is to keep it lean so that we can highlight the transients functionality of the plugin.
Note that all of the plugin's files can be retrieved from GitHub at any time. In the mean time, go ahead and create a directory called 'top-commenters-cached' and make sure that it has the following directory structure:
If you're not interested in localizing the plugin, feel free to leave the 'lang' directory out of the plugin. At this point, we're ready to begin writing the widget.
Basic Functionality
The plugin is simple. It should…
- Allow the user to give the widget a custom
- Retrieve the top 10 most popular commenters of the life of the blog
Easy enough. Here's the code for the basic plugin. Note that it's commented throughout so spend sometime reading through it to understand what we're doing. If you're unfamiliar with the widget API, don't forget to check out our WordPress Widget Boilerplate post.
class Top_Commenters_Cached extends WP_Widget { const name = 'Top Commenters (Cached!)'; const locale = 'top-commenters-cached-locale'; const slug = 'top-commenters-cached'; /*--------------------------------------------------*/ /* Constructor /*--------------------------------------------------*/ /** * The widget constructor. Specifies the classname and description, instantiates * the widget, loads localization files, and includes necessary scripts and * styles. */ function Top_Commenters_Cached() { $widget_opts = array ( 'classname' => self::name, 'description' => __('A plugin used to demonstrate the WordPress Transients API for an Envato blog series.', self::locale) ); $this->WP_Widget(self::slug, __(self::name, self::locale), $widget_opts); load_plugin_textdomain(self::locale, false, dirname(plugin_basename( __FILE__ ) ) . '/lang/' ); } // end constructor /*--------------------------------------------------*/ /* API Functions /*--------------------------------------------------*/ /** * Outputs the content of the widget. * * @args The array of form elements * @instance */ function widget($args, $instance) { extract($args, EXTR_SKIP); echo $before_widget; $widget_title = empty($instance['widget_title']) ? '' : apply_filters('widget_title', $instance['widget_title']); $commenters = $this->query_for_commenters(); // Display the widget include(WP_PLUGIN_DIR . '/' . self::slug . '/views/widget.php'); echo $after_widget; } // end widget /** * Processes the widget's options to be saved. * * @new_instance The previous instance of values before the update. * @old_instance The new instance of values to be generated via the update. */ function update($new_instance, $old_instance) { $instance = $old_instance; $instance['widget_title'] = $this->strip($new_instance, 'widget_title'); return $instance; } // end widget /** * Generates the administration form for the widget. * * @instance The array of keys and values for the widget. */ function form($instance) { $instance = wp_parse_args( (array)$instance, array( 'widget_title' => '' ) ); $widget_title = $this->strip($instance, 'widget_title'); // Display the admin form include(WP_PLUGIN_DIR . '/' . self::slug . '/views/admin.php'); } // end form /*--------------------------------------------------*/ /* Private Functions /*--------------------------------------------------*/ /** * Retrieves the weekly top commenters for the past week and stores the values in the cache. * If the cache is empty, then the function will request information from the database and * store it in the cache. */ private function query_for_commenters() { $commenters = null; // query the database for the top commenters global $wpdb; $commenters = $wpdb->get_results(" select count(comment_author) as comments_count, comment_author, comment_type from $wpdb->comments where comment_type != 'pingback' and comment_author != '' and comment_approved = '1' group by comment_author order by comment_author desc LIMIT 10 "); return $commenters } // end query_for_commenters /*--------------------------------------------------*/ /* Helper Functions /*--------------------------------------------------*/ /** * Convenience method for stripping tags and slashes from the content * of a form input. * * @obj The instance of the argument array * @title The title of the element from which we're stripping tags and slashes. */ private function strip($obj, $title) { return strip_tags(stripslashes($obj[$title])); } // end strip } // end class add_action('widgets_init', create_function('', 'register_widget("Top_Commenters_Cached");')); ?>
Next, let's take a look at the widget's view. This is the part of the plugin that's responsible for displaying the list of comments. It works by displaying the widget's title (if it's defined), then loops through the results creating a new list item.
<?php if(strlen(trim($widget_title)) > 0) { ?> <h3 class="widget-title"> <?php echo $widget_title; ?> </h3> <?php } // end if global $wpdb; $comment_list = '<ol>'; foreach($commenters as $commenter) { $comment_list .= '<li>'; // actually print the commenter's name and the number of comments $comment_list .= $commenter->comment_author; $comment_list .= ' (' . $commenter->comments_count . ')'; $comment_list .= '</li>'; } // end foreach $comment_list .= '</ol>'; echo $comment_list; ?>
Obviously, we've left out part of the code. Namely, the admin panel. It should simply allow for users to enter a title for their widget:
<div> <fieldset> <legend> <?php _e('Widget Options', self::locale); ?> </legend> <label for="<?php echo $this->get_field_id('widget_title'); ?>" class="block"> <?php _e('Title:', self::locale); ?> </label> <input type="text" name="<?php echo $this->get_field_name('widget_title'); ?>" id="<?php echo $this->get_field_id('widget_title'); ?>" value="<?php echo $instance['widget_title']; ?>" class="" /> </fieldset> </div>
Remember that you can view the full source code and download the plugin from its GitHub repository.
Cache The Data
At this point, we have a functional plugin; however, we're not actually caching any data yet. The most intensive part of this plugin is when we're querying the database and the results of the query are what we actually want to cache so let's do that.
Locate the query in the code:
global $wpdb; $commenters = $wpdb->get_results(" select count(comment_author) as comments_count, comment_author, comment_type from $wpdb->comments where comment_type != 'pingback' and comment_author != '' and comment_approved = '1' group by comment_author order by comment_author desc LIMIT 10 ");
And let's store the results for 12 hours using the transients API:
set_transient('top_commenters_cached', $commenters, 60 * 60 * 12);
Pretty easy, right? Of course, we're not done yet.
Retrieve The Data
Once the transient is set, we need to be able to retrieve the transient. Let's set that up now:
private function query_for_commenters() { return get_transient('top_commenters_cached'); } // end query_for_commenters
That's all there is to it!
But wait - if you recall from the first post in the series, transients actually expire so we're not guaranteed to retrieve the transient.
Finding Missing Data
Regardless of what you're doing, retrieving data that has expired generally follows the same process:
- Check for the existence of the transient
- If it exists, use it
- If it doesn't exist, set it then retrieve it
So let's do that within the context of our plugin:
private function query_for_commenters() { $commenters = null; // check to see if the transient exists. set it if it's expired or missing if(!get_transient('top_commenters_cached')) { // query the database for the top commenters global $wpdb; $commenters = $wpdb->get_results(" select count(comment_author) as comments_count, comment_author, comment_type from $wpdb->comments where comment_type != 'pingback' and comment_author != '' and comment_approved = '1' group by comment_author order by comment_author desc LIMIT 10 "); // store the result set_transient('top_commenters_cached', $commenters, 60 * 60 * 12); } // end if // transient is guaranteed to exist now, so return it return get_transient('top_commenters_cached'); } // end query_for_commenters
Conclusion
Not too bad, right?
As you can see, working with the Transients API requires little more than knowing when to use it and what functions are available. In my opinion, it's one of the most powerful aspects of the WordPress API.
If you find yourself retrieving large amounts of data, looking for a way to expire data for a refresh, or simply wanting to take advantage of caching plugins, remember to take advantage of the Transients API.
Comments