Last active
December 5, 2023 17:57
-
-
Save ryanshoover/95947f4c4501a57cf5f6b3a6b360b5ca to your computer and use it in GitHub Desktop.
Add object caching to ACF
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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