1. Introduction
  2. What Are Transients?
  3. What Can We Store In The Cache?
  4. Cache API Class
  5. Usage
  6. How To Avoid Stale Data

Introduction

Implementing a caching system alongside WordPress' object cache is an easy way of boosting the performance of your web application. Post or page templates with lots of content on will lead to a lot of queries to the database to render the page. Our first thought for solving this problem usually involves reaching for one of the plethora of caching plugins out there, however, with the careful usage of the built in transient system we can avoid the need to install one of these plugins.

Plugins such as Advanced Custom Fields are great when developing the admin interfaces a client requires but when it comes to the queries on the front-end they leave a lot to be desired. Specifically the Repeater field from this plugin will cause a ridiculous amount of queries to the database when grabbing the field content. How do we solve this? Caching!

What Are Transients?

Transients are essentially key value pairs stored in the wp_options table. Each transient has a expiry time and a value, both of which are stored separately in the options table, prefixed with _transient_.

With that in mind your theme or plugin is still going to have to have make at least two database queries to retrieve an entry from the cache because WordPress will query the value and then query the table for the expiration time.

What Can We Store In The Cache?

We can store regular variables, arrays or Objects. The built in transient API handles serialization for us.

Cache API Class

As WordPress only provides get and set methods for transients creating our own API over the top is paramount to maintaining code readability.

Below we have created a Cache class with the method remember. You will need to include this file in your functions.php file if you're not using a composer class map.

<?php

class Cache
{
    public static function remember(string $key, int $ttl, Closure $closure): mixed
    {
        if (false === ($content = get_transient($key))) {
            $content = $closure();
            set_transient($key, $content, $ttl);
        }

        return $content;
    }
}

I also find it helpful to declare constants making expiration seconds easier to calculate. You can define these in your functions.php file if you wish.

<?php

if ( ! defined( 'SECONDS_IN_AN_HOUR' ) ) {
	define( 'SECONDS_IN_AN_HOUR', 3600 );
}

if ( ! defined( 'SECONDS_IN_A_DAY' ) ) {
	define( 'SECONDS_IN_A_DAY', 86400 );
}

if ( ! defined( 'SECONDS_IN_A_WEEK' ) ) {
	define( 'SECONDS_IN_A_WEEK', 604800 );
}

// Usage: 2 * SECONDS_IN_A_DAY

Usage

If we want to store a value in the cache for two hours then we can use the remember method like so:

<?php

$value = Cache::remember( 'key', 2 * SECONDS_IN_AN_HOUR, function (): string {
    // a large number of queries...
    return 'Content from the database';
} );

echo $value;

Caching values wherever possible will massively improve performance!

How To Avoid Stale Data

One caveat to caching like this from within your plugin or theme is that data can become stale. With no simply button for the user to press on the admin side of WordPress to clear the cache, we need to provide a way for the user to clear the cache or for the cache to automatically be cleared.

In many of my own projects I implement a cache clearing function that will clear specific keys when a post is updated. As the page content is all set using the post editor we can safely clear the keys and expect new data to be there.

By hooking into the post_updated hook we can call our cache clearing function. The clear_transients function will be sent the post ID and then use that ID to try and call a function in the cache class, for example if you just updated a page with and ID of 567 clear_567_transients would be called. If that function doesn't exist the function returns.

In this example we can make a query to find the ID of the page that is assigned to be our front page and assign that to the clear_front_page_transients function.

<?php
// functions.php
add_action('save_post', [Cache::class, 'clear_transients'], 10, 1);

// Cache.php
class Cache
{
    public static function remember(string $key, int $ttl, Closure $closure): mixed
    {
        if (false === ($content = get_transient($key))) {
            $content = $closure();
            set_transient($key, $content, $ttl);
        }

        return $content;
    }

    protected static function clear_front_page_transients(): void {
        $transient_keys = [
            'my_front_page_cache_key',
        ];

        foreach ($transient_keys as $key) {
            delete_transient($key);
        }
    }

    public static function clear_transients(int $post_id): void {
        $front_page = (int) get_option('page_on_front');

        $callback = match ($id) {
            $front_page => 'clear_front_page_transients',
            default => "clear_{$id}_transients"
        };

        if ( ! is_callable([static::class, $callback])) {
            return;
        }

        call_user_func([static::class, $callback]);
    }
}

Copyright © 2024 | bonnick.dev