From 3f4bab922f0a805f9e62f58e89dc3f181892d777 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:12:04 +0200 Subject: [PATCH 1/4] Performance: prime sibling field key lookups in one query Resolving a field key runs one query per field. When a field belongs to a field group, fetch all of the group's published fields in a single query and cache their key lookups, so sibling lookups avoid further queries: get_field() over a 50-field template drops from ~101 queries / ~17ms to ~5 / ~2.8ms on cache-less hosts. The priming query runs with suppress_filters => false, matching the per-key lookup in acf_get_field_post() that it primes, so per-language / context query filters (WPML, Polylang, ACFML) still run and resolve the same posts. Opt-out via the acf/prime_field_group_fields filter. Split from the broader performance PR for focused review because it touches the field-value load path and interacts with multilingual plugins. Co-Authored-By: Claude Opus 4.8 (1M context) --- includes/acf-field-functions.php | 84 +++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/includes/acf-field-functions.php b/includes/acf-field-functions.php index 308c57cf..20e67b47 100644 --- a/includes/acf-field-functions.php +++ b/includes/acf-field-functions.php @@ -162,7 +162,14 @@ function acf_get_field_post( $id = 0 ) { // Check $post_id and return the post when possible. if ( $post_id ) { - return get_post( $post_id ); + $post = get_post( $post_id ); + + // Prime sibling field lookups to avoid one query per field. + if ( $post && $post->post_parent ) { + _acf_prime_sibling_field_posts( $post ); + } + + return $post; } } @@ -170,6 +177,81 @@ function acf_get_field_post( $id = 0 ) { return false; } +/** + * Primes the field key lookup cache for all sibling fields of the given field post. + * + * Resolving a field key normally runs one query per field. When a field belongs + * to a field group, all of the group's fields are fetched in a single query and + * their key lookups are cached, so subsequent sibling key lookups avoid further + * queries. + * + * The priming query runs with `suppress_filters => false`, matching the per-key + * lookup it replaces, so plugins that filter field queries per language/context + * (e.g. WPML, Polylang) still run against it and resolve the same posts they + * would have for the individual lookups. + * + * @since SCF 6.9.0 + * + * @param WP_Post $post The field post object. + * @return void + */ +function _acf_prime_sibling_field_posts( $post ) { + static $primed = array(); + + // Bail early if this parent was already primed during this request. + if ( isset( $primed[ $post->post_parent ] ) ) { + return; + } + + /** + * Filters whether sibling field lookups are primed when a field is loaded by key or name. + * + * Priming collapses the per-field key lookups into a single query; it does + * not bypass query filters, but can be disabled here if needed. + * + * @since SCF 6.9.0 + * + * @param boolean $prime True to prime sibling field lookups. Default true. + */ + if ( ! apply_filters( 'acf/prime_field_group_fields', true ) ) { + return; + } + + // Only prime when the parent is a field group. + $parent = get_post( $post->post_parent ); + if ( ! $parent || 'acf-field-group' !== $parent->post_type ) { + return; + } + + $primed[ $post->post_parent ] = true; + + // Fetch all of the group's published fields in a single query, keeping + // suppress_filters => false so third-party query filters still run (matching + // the per-key lookup in acf_get_field_post() that this primes). + $field_posts = get_posts( + array( + 'posts_per_page' => -1, + 'post_type' => 'acf-field', + 'post_parent' => $parent->ID, + 'post_status' => 'publish', + 'orderby' => 'menu_order title', + 'order' => 'ASC', + 'suppress_filters' => false, + 'cache_results' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + + foreach ( $field_posts as $field_post ) { + // The field key is stored as the post_name. + $cache_key = acf_cache_key( "acf_get_field_post:key:{$field_post->post_name}" ); + if ( false === wp_cache_get( $cache_key, 'secure-custom-fields' ) ) { + wp_cache_set( $cache_key, $field_post->ID, 'secure-custom-fields' ); + } + } +} + /** * acf_is_field_key * From 61bf8080537f75677a304163fe786b8ad76ef407 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 2 Jul 2026 16:08:12 +0200 Subject: [PATCH 2/4] Address review findings on sibling field priming - Prime only on a cache miss so warm persistent-cache requests stay query-free instead of paying an extra query per field group. - Resolve sibling keys with a global post_name__in query (same ordering and filter semantics as the per-key lookup) so keys duplicated across field groups resolve to the same post as before. - Source sibling keys from acf_get_raw_fields(), reusing its cached bulk fetch instead of running a near-duplicate group-scoped query. - Memoize the parent before any bails so repeater/group sub-field lookups don't re-run the filter and parent checks per sibling. - Cap priming at 100 keys so very large groups don't hydrate unbounded post sets; remaining keys fall back to per-key lookups. - Use wp_cache_add() instead of wp_cache_get() + wp_cache_set(). - Rename to _scf_prime_sibling_field_posts() and scf/prime_field_group_fields per the scf_ naming convention for new functions and hooks. - Correct @since to 6.9.2. Co-Authored-By: Claude Fable 5 --- includes/acf-field-functions.php | 84 ++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/includes/acf-field-functions.php b/includes/acf-field-functions.php index 20e67b47..ced3fb3d 100644 --- a/includes/acf-field-functions.php +++ b/includes/acf-field-functions.php @@ -158,18 +158,18 @@ function acf_get_field_post( $id = 0 ) { // Update cache. wp_cache_set( $cache_key, $post_id, 'secure-custom-fields' ); + + // A cache miss means sibling field lookups are likely cold too: + // prime them in bulk to avoid one query per field. Cache hits skip + // this so warm requests stay query-free. + if ( $posts && $posts[0]->post_parent ) { + _scf_prime_sibling_field_posts( $posts[0] ); + } } // Check $post_id and return the post when possible. if ( $post_id ) { - $post = get_post( $post_id ); - - // Prime sibling field lookups to avoid one query per field. - if ( $post && $post->post_parent ) { - _acf_prime_sibling_field_posts( $post ); - } - - return $post; + return get_post( $post_id ); } } @@ -181,39 +181,45 @@ function acf_get_field_post( $id = 0 ) { * Primes the field key lookup cache for all sibling fields of the given field post. * * Resolving a field key normally runs one query per field. When a field belongs - * to a field group, all of the group's fields are fetched in a single query and - * their key lookups are cached, so subsequent sibling key lookups avoid further - * queries. + * to a field group, the keys of all the group's fields are resolved with a single + * query and cached, so subsequent sibling key lookups avoid further queries. * - * The priming query runs with `suppress_filters => false`, matching the per-key - * lookup it replaces, so plugins that filter field queries per language/context - * (e.g. WPML, Polylang) still run against it and resolve the same posts they - * would have for the individual lookups. + * The priming query keeps the semantics of the per-key lookup in + * acf_get_field_post(): it is not scoped to the field group, so a key that + * exists in more than one group resolves to the same post either way; it runs + * with `suppress_filters => false`, so plugins that filter field queries per + * language/context (e.g. WPML, Polylang) still apply; and cache keys are built + * per sibling with acf_cache_key(), so language-scoped cache keys behave as + * they do for individual lookups. * - * @since SCF 6.9.0 + * @since SCF 6.9.2 * * @param WP_Post $post The field post object. * @return void */ -function _acf_prime_sibling_field_posts( $post ) { +function _scf_prime_sibling_field_posts( $post ) { static $primed = array(); - // Bail early if this parent was already primed during this request. + // Bail early if this parent was already handled during this request. Marking + // it up front also memoizes non-group parents (e.g. repeater sub-fields), so + // their repeated lookups skip the checks below. if ( isset( $primed[ $post->post_parent ] ) ) { return; } + $primed[ $post->post_parent ] = true; /** * Filters whether sibling field lookups are primed when a field is loaded by key or name. * * Priming collapses the per-field key lookups into a single query; it does - * not bypass query filters, but can be disabled here if needed. + * not bypass query filters, but can be disabled here if needed. Runs once + * per field parent per request. * - * @since SCF 6.9.0 + * @since SCF 6.9.2 * * @param boolean $prime True to prime sibling field lookups. Default true. */ - if ( ! apply_filters( 'acf/prime_field_group_fields', true ) ) { + if ( ! apply_filters( 'scf/prime_field_group_fields', true ) ) { return; } @@ -223,17 +229,32 @@ function _acf_prime_sibling_field_posts( $post ) { return; } - $primed[ $post->post_parent ] = true; + // Collect the sibling field keys from the group's raw fields. This reuses + // (and warms) the same cached bulk fetch used when the group is rendered, + // rather than running a second group-scoped query. + $keys = array(); + foreach ( acf_get_raw_fields( $parent->ID ) as $raw_field ) { + if ( ! empty( $raw_field['key'] ) ) { + $keys[] = $raw_field['key']; + } + } - // Fetch all of the group's published fields in a single query, keeping - // suppress_filters => false so third-party query filters still run (matching - // the per-key lookup in acf_get_field_post() that this primes). + // Cap the primed keys so pathologically large groups don't hydrate + // thousands of posts at once; the rest fall back to per-key lookups. + $keys = array_slice( array_unique( $keys ), 0, 100 ); + if ( ! $keys ) { + return; + } + + // Resolve all keys in one query with the same semantics as the per-key + // lookup in acf_get_field_post(): global (not group-scoped), same ordering, + // and suppress_filters => false so third-party query filters still run. + // The post_name__in clause bounds the result set. $field_posts = get_posts( array( 'posts_per_page' => -1, 'post_type' => 'acf-field', - 'post_parent' => $parent->ID, - 'post_status' => 'publish', + 'post_name__in' => $keys, 'orderby' => 'menu_order title', 'order' => 'ASC', 'suppress_filters' => false, @@ -244,11 +265,12 @@ function _acf_prime_sibling_field_posts( $post ) { ); foreach ( $field_posts as $field_post ) { - // The field key is stored as the post_name. + // The field key is stored as the post_name. The first post per key in + // the ordered result matches what the per-key query would select, and + // wp_cache_add() keeps it without overwriting later duplicates or + // entries cached by earlier lookups. $cache_key = acf_cache_key( "acf_get_field_post:key:{$field_post->post_name}" ); - if ( false === wp_cache_get( $cache_key, 'secure-custom-fields' ) ) { - wp_cache_set( $cache_key, $field_post->ID, 'secure-custom-fields' ); - } + wp_cache_add( $cache_key, $field_post->ID, 'secure-custom-fields' ); } } From a6962322cbe5a7963cf4d00b777ad0b6c88ea9a5 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 2 Jul 2026 17:20:32 +0200 Subject: [PATCH 3/4] Lowercase field lookup cache keys to match stored slugs Field key/name lookups compare against post_name/post_excerpt, which is case-insensitive under WordPress's default collations, and stored slugs are always sanitized to lowercase. The lookup cache keyed entries by the requested string, so a mixed-case request never hit entries primed under the stored slug, and acf_flush_field_cache() (which keys by the stored identity) could not invalidate them. Lowercasing the cache key at the lookup, priming, and flush sites makes all three agree. Co-Authored-By: Claude Fable 5 --- includes/acf-field-functions.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/includes/acf-field-functions.php b/includes/acf-field-functions.php index ced3fb3d..7bf1d7cd 100644 --- a/includes/acf-field-functions.php +++ b/includes/acf-field-functions.php @@ -133,8 +133,11 @@ function acf_get_field_post( $id = 0 ) { // Determine id type. $type = acf_is_field_key( $id ) ? 'key' : 'name'; - // Try cache. - $cache_key = acf_cache_key( "acf_get_field_post:$type:$id" ); + // Try cache. The lookup string is lowercased to match both the + // case-insensitive post_name/post_excerpt comparison in the query + // below and the stored (sanitized, lowercase) slug that priming and + // acf_flush_field_cache() key by. + $cache_key = acf_cache_key( "acf_get_field_post:$type:" . strtolower( $id ) ); $post_id = wp_cache_get( $cache_key, 'secure-custom-fields' ); if ( $post_id === false ) { @@ -269,7 +272,7 @@ function _scf_prime_sibling_field_posts( $post ) { // the ordered result matches what the per-key query would select, and // wp_cache_add() keeps it without overwriting later duplicates or // entries cached by earlier lookups. - $cache_key = acf_cache_key( "acf_get_field_post:key:{$field_post->post_name}" ); + $cache_key = acf_cache_key( 'acf_get_field_post:key:' . strtolower( $field_post->post_name ) ); wp_cache_add( $cache_key, $field_post->ID, 'secure-custom-fields' ); } } @@ -1236,9 +1239,10 @@ function acf_flush_field_cache( $field ) { // Delete stored data. acf_get_store( 'fields' )->remove( $field['key'] ); - // Flush cached post_id for this field's name and key. - wp_cache_delete( acf_cache_key( "acf_get_field_post:name:{$field['name']}" ), 'secure-custom-fields' ); - wp_cache_delete( acf_cache_key( "acf_get_field_post:key:{$field['key']}" ), 'secure-custom-fields' ); + // Flush cached post_id for this field's name and key, lowercased to match + // how acf_get_field_post() keys its cache. + wp_cache_delete( acf_cache_key( 'acf_get_field_post:name:' . strtolower( $field['name'] ) ), 'secure-custom-fields' ); + wp_cache_delete( acf_cache_key( 'acf_get_field_post:key:' . strtolower( $field['key'] ) ), 'secure-custom-fields' ); // Flush cached array of post_ids for this field's parent. wp_cache_delete( acf_cache_key( "acf_get_field_posts:{$field['parent']}" ), 'secure-custom-fields' ); From bffe95739f5bd2eab8999c4e07a78c39a4c1c16a Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 2 Jul 2026 18:23:07 +0200 Subject: [PATCH 4/4] Address second-round review findings on sibling priming - Flush the field lookup cache in acf_trash_field() (matching untrash/ update/delete) so trashed fields stop resolving from persistent caches. Pre-existing gap that priming would have widened. - Defer priming to the second cache miss per parent so single-field requests keep paying only their own query. - Prime only on key-type lookups; name misses paid the priming cost without ever benefiting since only key entries are written. - Batch the sibling cache writes with wp_cache_add_multiple(). - Skip trashed siblings when collecting keys so they don't consume the priming cap. - Batch-fetch evicted posts in acf_get_raw_fields() via _prime_post_caches() instead of one query per field. - Extract _scf_field_post_cache_key() and _scf_field_post_query_args() so the cache-key format and query semantics shared by lookup, priming, and flushing live in one place. Co-Authored-By: Claude Fable 5 --- includes/acf-field-functions.php | 161 +++++++++++++++++++++---------- 1 file changed, 111 insertions(+), 50 deletions(-) diff --git a/includes/acf-field-functions.php b/includes/acf-field-functions.php index 7bf1d7cd..52571709 100644 --- a/includes/acf-field-functions.php +++ b/includes/acf-field-functions.php @@ -110,6 +110,48 @@ function acf_get_raw_field( $id = 0 ) { return $field; } +/** + * Builds the cache key for a field post lookup. + * + * The identifier is lowercased to match both the case-insensitive + * post_name/post_excerpt comparison in the lookup query and the stored + * (sanitized, lowercase) slug, so lookups, sibling priming, and + * acf_flush_field_cache() all agree on one key per field. + * + * @since SCF 6.9.2 + * + * @param string $type The lookup type: 'key' or 'name'. + * @param string $id The field key or name. + * @return string The cache key. + */ +function _scf_field_post_cache_key( $type, $id ) { + return acf_cache_key( "acf_get_field_post:$type:" . strtolower( $id ) ); +} + +/** + * Returns the shared get_posts() arguments for field post lookups. + * + * The per-key lookup in acf_get_field_post() and the sibling priming query + * must keep identical semantics — the ordering decides which post wins when + * a key exists more than once, and `suppress_filters => false` keeps + * third-party query filters running — so both build on these arguments. + * + * @since SCF 6.9.2 + * + * @return array get_posts() arguments. + */ +function _scf_field_post_query_args() { + return array( + 'post_type' => 'acf-field', + 'orderby' => 'menu_order title', + 'order' => 'ASC', + 'suppress_filters' => false, + 'cache_results' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ); +} + /** * acf_get_field_post * @@ -133,26 +175,19 @@ function acf_get_field_post( $id = 0 ) { // Determine id type. $type = acf_is_field_key( $id ) ? 'key' : 'name'; - // Try cache. The lookup string is lowercased to match both the - // case-insensitive post_name/post_excerpt comparison in the query - // below and the stored (sanitized, lowercase) slug that priming and - // acf_flush_field_cache() key by. - $cache_key = acf_cache_key( "acf_get_field_post:$type:" . strtolower( $id ) ); + // Try cache. + $cache_key = _scf_field_post_cache_key( $type, $id ); $post_id = wp_cache_get( $cache_key, 'secure-custom-fields' ); if ( $post_id === false ) { // Query posts. $posts = get_posts( - array( - 'posts_per_page' => 1, - 'post_type' => 'acf-field', - 'orderby' => 'menu_order title', - 'order' => 'ASC', - 'suppress_filters' => false, - 'cache_results' => true, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - "acf_field_$type" => $id, + array_merge( + _scf_field_post_query_args(), + array( + 'posts_per_page' => 1, + "acf_field_$type" => $id, + ) ) ); @@ -163,9 +198,11 @@ function acf_get_field_post( $id = 0 ) { wp_cache_set( $cache_key, $post_id, 'secure-custom-fields' ); // A cache miss means sibling field lookups are likely cold too: - // prime them in bulk to avoid one query per field. Cache hits skip + // prime them in bulk to avoid one query per field. Only key + // lookups are primed (priming writes key entries, so name misses + // would pay its cost without ever benefiting); cache hits skip // this so warm requests stay query-free. - if ( $posts && $posts[0]->post_parent ) { + if ( 'key' === $type && $posts && $posts[0]->post_parent ) { _scf_prime_sibling_field_posts( $posts[0] ); } } @@ -181,13 +218,16 @@ function acf_get_field_post( $id = 0 ) { } /** - * Primes the field key lookup cache for all sibling fields of the given field post. + * Primes the field key lookup cache for the sibling fields of the given field post. * - * Resolving a field key normally runs one query per field. When a field belongs - * to a field group, the keys of all the group's fields are resolved with a single - * query and cached, so subsequent sibling key lookups avoid further queries. + * Resolving a field key normally runs one query per field. When key lookups + * for two fields of the same field group miss the cache in one request, the + * keys of all the group's published fields are resolved in bulk and cached, + * so further sibling key lookups avoid their per-field queries. Priming is + * deferred to the second miss so requests that resolve a single field keep + * paying only that field's query. * - * The priming query keeps the semantics of the per-key lookup in + * The bulk query keeps the semantics of the per-key lookup in * acf_get_field_post(): it is not scoped to the field group, so a key that * exists in more than one group resolves to the same post either way; it runs * with `suppress_filters => false`, so plugins that filter field queries per @@ -202,6 +242,7 @@ function acf_get_field_post( $id = 0 ) { */ function _scf_prime_sibling_field_posts( $post ) { static $primed = array(); + static $missed = array(); // Bail early if this parent was already handled during this request. Marking // it up front also memoizes non-group parents (e.g. repeater sub-fields), so @@ -209,10 +250,17 @@ function _scf_prime_sibling_field_posts( $post ) { if ( isset( $primed[ $post->post_parent ] ) ) { return; } + + // Defer priming until a second sibling of the same parent misses the + // cache, so single-field requests skip the priming queries entirely. + if ( ! isset( $missed[ $post->post_parent ] ) ) { + $missed[ $post->post_parent ] = true; + return; + } $primed[ $post->post_parent ] = true; /** - * Filters whether sibling field lookups are primed when a field is loaded by key or name. + * Filters whether sibling field lookups are primed when a field is loaded by key. * * Priming collapses the per-field key lookups into a single query; it does * not bypass query filters, but can be disabled here if needed. Runs once @@ -232,12 +280,14 @@ function _scf_prime_sibling_field_posts( $post ) { return; } - // Collect the sibling field keys from the group's raw fields. This reuses - // (and warms) the same cached bulk fetch used when the group is rendered, - // rather than running a second group-scoped query. + // Collect the published sibling field keys from the group's raw fields. + // This reuses (and warms) the same cached bulk fetch used when the group + // is rendered, rather than running a second group-scoped query. Trashed + // siblings are skipped so they don't consume the priming cap with keys + // the publish-only bulk query below can never match. $keys = array(); foreach ( acf_get_raw_fields( $parent->ID ) as $raw_field ) { - if ( ! empty( $raw_field['key'] ) ) { + if ( ! empty( $raw_field['key'] ) && 'publish' === get_post_status( $raw_field['ID'] ) ) { $keys[] = $raw_field['key']; } } @@ -250,31 +300,32 @@ function _scf_prime_sibling_field_posts( $post ) { } // Resolve all keys in one query with the same semantics as the per-key - // lookup in acf_get_field_post(): global (not group-scoped), same ordering, - // and suppress_filters => false so third-party query filters still run. - // The post_name__in clause bounds the result set. + // lookup in acf_get_field_post(). The post_name__in clause bounds the + // result set. $field_posts = get_posts( - array( - 'posts_per_page' => -1, - 'post_type' => 'acf-field', - 'post_name__in' => $keys, - 'orderby' => 'menu_order title', - 'order' => 'ASC', - 'suppress_filters' => false, - 'cache_results' => true, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, + array_merge( + _scf_field_post_query_args(), + array( + 'posts_per_page' => -1, + 'post_name__in' => $keys, + ) ) ); + // Collect key => post ID pairs, keeping the first post per key: the field + // key is stored as the post_name, and the first post per key in the + // ordered result matches what the per-key query would select. + $adds = array(); foreach ( $field_posts as $field_post ) { - // The field key is stored as the post_name. The first post per key in - // the ordered result matches what the per-key query would select, and - // wp_cache_add() keeps it without overwriting later duplicates or - // entries cached by earlier lookups. - $cache_key = acf_cache_key( 'acf_get_field_post:key:' . strtolower( $field_post->post_name ) ); - wp_cache_add( $cache_key, $field_post->ID, 'secure-custom-fields' ); + $cache_key = _scf_field_post_cache_key( 'key', $field_post->post_name ); + if ( ! isset( $adds[ $cache_key ] ) ) { + $adds[ $cache_key ] = $field_post->ID; + } } + + // A single batched add avoids one cache round-trip per sibling on + // external object caches and never overwrites existing entries. + wp_cache_add_multiple( $adds, 'secure-custom-fields' ); } /** @@ -541,6 +592,13 @@ function acf_get_raw_fields( $id = 0 ) { wp_cache_set( $cache_key, $post_ids, 'secure-custom-fields' ); } + // Batch-fetch any posts missing from the object cache before the loop + // hydrates them individually: a warm ID list with evicted post entries + // would otherwise cost one query per field. + if ( $post_ids ) { + _prime_post_caches( $post_ids, false, false ); + } + // Loop over ids and populate array of fields. $fields = array(); foreach ( $post_ids as $post_id ) { @@ -1239,10 +1297,9 @@ function acf_flush_field_cache( $field ) { // Delete stored data. acf_get_store( 'fields' )->remove( $field['key'] ); - // Flush cached post_id for this field's name and key, lowercased to match - // how acf_get_field_post() keys its cache. - wp_cache_delete( acf_cache_key( 'acf_get_field_post:name:' . strtolower( $field['name'] ) ), 'secure-custom-fields' ); - wp_cache_delete( acf_cache_key( 'acf_get_field_post:key:' . strtolower( $field['key'] ) ), 'secure-custom-fields' ); + // Flush cached post_id for this field's name and key. + wp_cache_delete( _scf_field_post_cache_key( 'name', $field['name'] ), 'secure-custom-fields' ); + wp_cache_delete( _scf_field_post_cache_key( 'key', $field['key'] ), 'secure-custom-fields' ); // Flush cached array of post_ids for this field's parent. wp_cache_delete( acf_cache_key( "acf_get_field_posts:{$field['parent']}" ), 'secure-custom-fields' ); @@ -1316,6 +1373,10 @@ function acf_trash_field( $id = 0 ) { // Trash post. wp_trash_post( $field['ID'] ); + // Flush field cache so lookups by key or name stop resolving the trashed + // post (matching acf_untrash_field, which flushes on restore). + acf_flush_field_cache( $field ); + /** * Fires immediately after a field has been trashed. *