Skip to content

Instantly share code, notes, and snippets.

@ryanshoover
Last active December 5, 2023 17:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryanshoover/95947f4c4501a57cf5f6b3a6b360b5ca to your computer and use it in GitHub Desktop.
Save ryanshoover/95947f4c4501a57cf5f6b3a6b360b5ca to your computer and use it in GitHub Desktop.
Add object caching to ACF
<?php
/**
* ACF object caching
*
* Adds Object caching support to ACF so taht it doesn't have to recalculate all of
* its data on each page load. It can substantially improve performance on sites that
* heavily use ACF.
*
* This is NOT multisite compatible. ACF uses an alternative data structure for multisites.
* Its internal APIs don't support an external tool to cache all of the multisite data.
*
* @package Pantheon\ACF_Object_Caching
* @author Pantheon Professional Servies Application Performance Team
* @license GPL-2
*
* @wordpress-plugin
* Plugin Name: ACF object caching
* Plugin URI: https://pantheon.io
* Description: Adds object caching to ACF for performance gains. NOT multisite compatible.
* Version: 1.0.0
* Author: Pantheon - Professional Services Application Performance Team
* Author URI: https://pantheon.io
* Text Domain: TextDomain
* License: GPL-2
*/
namespace Pantheon\ACF_Object_Caching;
/**
* Flush all values from the ACF cache group.
*/
function purge_acf_cache_group() {
if ( function_exists( 'wp_cache_flush_group' ) ) {
\wp_cache_flush_group( 'acf' );
}
}
register_activation_hook( __FILE__, __NAMESPACE__ . '\purge_acf_cache_group' );
register_deactivation_hook( __FILE__, __NAMESPACE__ . '\purge_acf_cache_group' );
/**
* ACF saves all of its data in runtime-only "stores" to improve performance.
* This restores those "stores" of data that were saved in object cache.
*/
function restore_stores() {
global $acf_stores;
// `store_hashes` is a master list of all the stores we're, well, storing
$store_hashes = \wp_cache_get( 'archived-store-hashes', 'acf' );
if ( ! is_array( $store_hashes ) ) {
return;
}
// Get each store's data and restore it.
foreach ( $store_hashes as $name => $old_hash ) {
$data = \wp_cache_get( 'archived-store:' . $name, 'acf' );
// The crc32 algorithm is used because of speed and reliabilty for data integrity checks.
$new_hash = hash( 'crc32c', json_encode( $data ) );
if ( $old_hash !== $new_hash ) {
error_log( 'ACF Object Caching: Hash mismatch for store ' . $name . ' before store generation. This store will not be restored.');
continue;
}
if ( $data !== false ) {
$store = \acf_register_store( $name, $data );
$new_hash = hash( 'crc32c', json_encode( $store->get_data() ) );
if ( $old_hash !== $new_hash ) {
error_log( 'ACF Object Caching: Hash mismatch for store ' . $name . ' after store generation. This store will be deleted.');
unset( $acf_stores[ $name ] );
}
}
}
}
/**
* At the end of the process, take all the stores that have been created and save them.
*/
function save_stores() {
if ( ! apply_filters( 'acf-object-caching-save-stores', true ) ) {
return;
}
global $acf_stores;
$new_stores = array_keys( $acf_stores );
// List of stores that shouldn't be cached.
$uncacheable_stores = apply_filters( 'acf-object-caching-uncacheable-stores', [ 'notices' ] );
// Combine our previous list of stores with the stores from this request.
$store_hashes = \wp_cache_get( 'archived-store-hashes', 'acf' );
$store_hashes = is_array( $store_hashes ) ? $store_hashes : [];
// Remove uncacheable stores from the list.
foreach ( $new_stores as $key => $store ) {
if ( in_array( $store, $uncacheable_stores, true ) ) {
unset( $new_stores[ $key ] );
}
}
// Save each store to the object cache.
foreach ( $acf_stores as $name => $store ) {
\wp_cache_set( 'archived-store:' . $name, $store->get_data(), 'acf' );
$store_hashes[ $name ] = hash( 'crc32c', json_encode( $store->get_data() ) );
}
// Save our master list of stores to object cache.
\wp_cache_set( 'archived-store-hashes', $store_hashes, 'acf' );
}
/**
* Purge the ACF values cache for a specific post.
*/
function purge_post_cache( $post_id ) {
$values = \wp_cache_get( 'archived-store-values', 'acf', false, $found );
// If we don't have a cached value, abort.
if ( ! $found ) {
return;
}
$values = is_array( $values ) ? $values : [];
// Loop through the values. If the key matches our pattern, remove it.
// For example, the pattern for field "date" on post number "123" is 123:date
foreach ( $values as $key => $value ) {
if ( strpos( $key, $post_id . ':' ) === 0 ) {
unset( $values[ $key ] );
}
}
// Set the stripped down values.
\wp_cache_set( 'archived-store-values', $values, 'acf' );
$stores = \wp_cache_get( 'archived-store-hashes', 'acf' );
if ( is_array( $stores ) ) {
$stores['values'] = hash( 'crc32c', json_encode( $values ) );
\wp_cache_set( 'archived-store-hashes', $stores, 'acf' );
}
}
// On init, restore the stores. Priority 6 to be compatible with ACF.
add_action( 'init', __NAMESPACE__ . '\restore_stores', 6 );
// On shutdown, save the stores. Priority 99 to make sure it fires late in the process.
add_action( 'shutdown', __NAMESPACE__ . '\save_stores', 99 );
// On save, delete the cached values for a post.
add_action( 'acf/save_post', __NAMESPACE__ . '\purge_post_cache' );
// On delete, delete the cached values for a post.
add_action( 'delete_post', __NAMESPACE__ . '\purge_post_cache' );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment