Site icon WebDevStudios

Working with Transients like a Boss

We’ve covered the reasons why using transients (and caching in general) can greatly enhance the performance of WordPress sites. I’m offering up what I find to be two compelling solutions for pain points that are often encountered when working with transients–how to create dynamic keys and delete transients in bulk.

Creating Dynamic Keys

Let’s say I have a function that accepts an array of three arguments and tries to get a transient that is specific to those parameters. If found, the transient’s data will be used. If not, the data will be regenerated. One common way to do this is as follows (see line 24 in particular):

/**
 * Get the number of posts.
 *
 * @param array $args {
 *     @type string $post_type   The post type.
 *     @type string $after_date  The earliest date to get posts for.
 *     @type string $before_date The latest date to get posts for.
 * }
 * @return int|bool              The number of posts.
 */
function wds_get_post_count( $args ) {

    // The default arguments.
    $defaults = array(
        'post_type'  => 'post',
        'after_date' => '2016-1-1',
        'before_date' => '',
     );

    // Merge arguments passed in with the defaults.
    $args = wp_parse_args( $args, $defaults );

    // Try to get data from transient.
    $post_count = get_transient( 'wds_post_count_' . $args['post_type'] . '_' . $args['after_date'] . '_' . $args['before_date']  );

    // If the transient was not found, regenerate the data.
    if ( ! $post_count ) {
        $post_count = wds_regenerate_post_count( $args );
    }

    return $post_count;
}

This results in dynamically created transient keys such as wds_post_count_post_2016-1-1_2016-7-1.

This technique is potentially problematic if you have values with spaces in them, arrays, or values such as true, false, or null. Extra checks would have to be in place to account for those possibilities and convert those values to something that can be used in the transient key every time we get, set, or delete it. Doing so can become messy and unwieldy–and the transient keys become longer and longer the more arguments you add to them. We can forego those unnecessary complications by replacing the code on line 24 with the following, instead:

$post_count = get_transient( 'wds_post_count_' . md5( serialize( $args ) )  );

serialize() will create a storable representation of a value, then passing that to md5() will create a unique hash of the data that’s always exactly 32 characters. Best of all, we can pass our entire $args array to those functions without the need to break out, validate, and add each of its values to the transient key individually. This results in keys such as:

wds_post_count_B3A076241698F988761FA618286DF384

Technically speaking, you could get rid of the wds_post_count_ prefix, but I recommend leaving it in so that part of the key is still human-readable and provides an indicator of what data it stores. Another bonus: If you’re using a GUI tool to view the database, you can sort the wp_options table by the option_name column, and all options with the name _transient_wds_post_count_* will be grouped together and easy to locate.

A complete example showing how to use serialize() and md5() when getting and setting transients is below.

<?php

/**
 * Get the number of posts.
 *
 * @param array $args {
 *     @type string $post_type   The post type.
 *     @type string $after_date  The earliest date to get posts for.
 *     @type string $before_date The latest date to get posts for.
 * }
 * @return int|bool              The number of posts.
 */
function wds_get_post_count( $args ) {

    // The default arguments.
    $defaults = array(
        'post_type'  => 'post',
        'after_date' => '2016-1-1',
        'before_date' => '',
     );

    // Merge arguments passed in with the defaults.
    $args = wp_parse_args( $args, $defaults );

    // Try to get data from transient.
    $post_count = get_transient( 'wds_post_count_' . md5( serialize( $args ) )  );

    // If the transient was not found, regenerate the data.
    if ( ! $post_count ) {
        $post_count = wds_regenerate_post_count( $args );
    }

    return $post_count;
}

/**
 * Regenerate the number of posts.
 *
 * @param array $args {
 *     @type string $post_type   The post type.
 *     @type string $after_date  The earliest date to get posts for.
 *     @type string $before_date The latest date to get posts for.
 * }
 * @return int|bool              The number of posts.
 */
function wds_regenerate_post_count( $args ) {

    // Run a query to get the data.
    $posts = new WP_Query( array(
        'post_type'  => $args['post_type'],
        'date_query' => array(
                'after'  => $args['after_date'],
                'before' => $args['before_date'],
            ),
    ) );

    // If posts were found,
    if ( $posts->have_posts() ) {

        // Save the data to a transient.
        set_transient( 'wds_post_count_' . md5( serialize( $args ) ), $posts->post_count, MONTH_IN_SECONDS );

        // Return the data.
        return $posts->post_count;
    }

    return false;
}

Deleting Transients in Bulk

If you’ve ever used WordPress’ wp_cache_* functions to set and get data using the object cache, you know that the $group parameter can be used to assign cached data to a group, then wp_cache_delete() can be used to easily delete the cached data within that group. Handy! When working with transients, we have no such luxury. Because of this, it’s common practice to loop through all possible key permutations, calling delete_transient() each time, like this:

// Get all post types.
$post_types = get_post_types();

// Loop through each post type.
foreach ( $post_types as $key => $post_type ) {

    // Get all possible values for after & before dates.
    $after_dates  = wds_get_post_count_after_dates();
    $before_dates = wds_get_post_count_before_dates();

    // Loop through all after dates.
    foreach ( $after_dates as $after_date ) {

        // Loop through all before dates.
        foreach ( $before_dates as $before_date ) {

            // Delete the transient.
            delete_transient( 'wds_post_count_' . $post_type . '_' . $after_date . '_' . $before_date );
        }
    }
}

As you can see, having several nested loops in place just to make sure all transient values are deleted is cumbersome, and can be prone to error in the event that even one permutation of the transient key isn’t accounted for. Instead of all of that, wouldn’t it be nice to just be able to delete all transients whose keys begin with wds_post_count? One of our resident WDS code wizards, Parbs, devised the set of functions below that can be used for just such a purpose.

/**
 * Delete all transients with a key prefix.
 *
 * @param string $prefix The key prefix.
 */
function wds_delete_transients( $prefix ) {
    wds_delete_transients_from_keys( wds_search_database_for_transients_by_prefix( $prefix ) );
}

/**
 * Searches the database for transients stored there that match a specific prefix.
 *
 * @param  string $prefix Prefix to search for.
 * @return array|bool     Nested array response for wpdb->get_results or false on failure.
 */
function wds_search_database_for_transients_by_prefix( $prefix ) {

    global $wpdb;

    // Add our prefix after concating our prefix with the _transient prefix
    $prefix = $wpdb->esc_like( '_transient_' . $prefix . '_' );

    // Build up our SQL query
    $sql = "SELECT `option_name` FROM $wpdb->options WHERE `option_name` LIKE '%s'";

    // Execute our query
    $transients = $wpdb->get_results( $wpdb->prepare( $sql, $prefix . '%' ), ARRAY_A );

    // If if looks good, pass it back
    if ( $transients && ! is_wp_error( $transients ) ) {
        return $transients;
    }

    // Otherise return false
    return false;
}

/**
 * Expects a passed in multidimensional array of transient keys.
 *
 * array(
 *     array( 'option_name' => '_transient_blah_blah' ),
 *     array( 'option_name' => 'transient_another_one' ),
 * )
 *
 * Can also pass in an array of transient names.
 *
 * @param  array|string $transients  Nested array of transients, keyed by option_name,
 *                                   or array of names of transients.
 * @return array|bool                Count of total vs deleted or false on failure.
 */
function wds_delete_transients_from_keys( $transients ) {

    if ( ! isset( $transients ) ) {
        return false;
    }

    // If we get a string key passed in, might as well use it correctly
    if ( is_string( $transients ) ) {
        $transients = array( array( 'option_name' => $transients ) );
    }

    // If its not an array, we can't do anything
    if ( ! is_array( $transients ) ) {
        return false;
    }

    $results = array();

    // Loop through our transients
    foreach ( $transients as $transient ) {

        if ( is_array( $transient ) ) {

            // If we have an array, grab the first element
            $transient = current( $transient );
        }

        // Remove that sucker
        $results[ $transient ] = delete_transient( str_replace( '_transient_', '', $transient ) );
    }

    // Return an array of total number, and number deleted
    return array(
        'total'   => count( $results ),
        'deleted' => array_sum( $results ),
    );
}

So putting those to use to delete our transients for this example would look something like this:

wds_delete_transients( 'wds_post_count' )

Quite a bit shorter and easier, eh?

Two words of caution:

  1. Since these functions search for and delete all transients that begin with the prefix you provide, make sure that you don’t have any other transient keys that begin with the same thing. For instance, if I were to save data with transient keys like wds_post_count_* and wds_post_count_featured_*, those would both be deleted if I were to call wds_delete_transients( 'wds_post_count' );
  2. Because the wds_search_database_for_transients_by_prefix() function searches the database for transients, it can’t be used on sites that have a persistent object cache in place. If a persistent object cache is being used, transients are stored in memory rather than in the database. So on those sites, you should delete transients using the full keys instead, such as delete_transient( 'transient_key_param1_param2' ).

Have any more?

I hope incorporating the techniques outlined in this post is as helpful for you as it has been for me when working with transients. Do you have some others? Let us know in the comments.

Exit mobile version