diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-comment-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-comment-meta-fields.php index 033ef9cd872f7..0f5f316c594ba 100644 --- a/src/wp-includes/rest-api/fields/class-wp-rest-comment-meta-fields.php +++ b/src/wp-includes/rest-api/fields/class-wp-rest-comment-meta-fields.php @@ -16,6 +16,14 @@ */ class WP_REST_Comment_Meta_Fields extends WP_REST_Meta_Fields { + /** + * Cache invalidation callback for comments. + * + * @since 6.9.0 + * @var string + */ + protected $cache_callback = 'wp_cache_set_comments_last_changed'; + /** * Retrieves the comment type for comment meta. * diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php index a9c3fbcde831a..d8fa768f44d27 100644 --- a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php +++ b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php @@ -15,6 +15,14 @@ #[AllowDynamicProperties] abstract class WP_REST_Meta_Fields { + /** + * Cache invalidation callback for the object type's last_changed key. + * + * @since 6.9.0 + * @var string + */ + protected $cache_callback = ''; + /** * Retrieves the object meta type. * @@ -143,6 +151,20 @@ public function update_value( $meta, $object_id ) { $fields = $this->get_registered_fields(); $error = new WP_Error(); + /* + * Temporarily unhook the last_changed cache invalidation to avoid + * firing it once per meta key during a batch update. + */ + $meta_type = $this->get_meta_type(); + $has_cache_callback = ! empty( $this->cache_callback ) + && function_exists( $this->cache_callback ); + + if ( $has_cache_callback ) { + remove_action( "added_{$meta_type}_meta", $this->cache_callback ); + remove_action( "updated_{$meta_type}_meta", $this->cache_callback ); + remove_action( "deleted_{$meta_type}_meta", $this->cache_callback ); + } + foreach ( $fields as $meta_key => $args ) { $name = $args['name']; if ( ! array_key_exists( $name, $meta ) ) { @@ -212,6 +234,17 @@ public function update_value( $meta, $object_id ) { } } + /* + * Re-hook the callback and call it once to invalidate the cache + * for the entire batch, rather than once per meta key. + */ + if ( $has_cache_callback ) { + add_action( "added_{$meta_type}_meta", $this->cache_callback ); + add_action( "updated_{$meta_type}_meta", $this->cache_callback ); + add_action( "deleted_{$meta_type}_meta", $this->cache_callback ); + call_user_func( $this->cache_callback ); + } + if ( $error->has_errors() ) { return $error; } diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-post-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-post-meta-fields.php index 4b1dc23b30b5f..456221b6350d0 100644 --- a/src/wp-includes/rest-api/fields/class-wp-rest-post-meta-fields.php +++ b/src/wp-includes/rest-api/fields/class-wp-rest-post-meta-fields.php @@ -24,6 +24,14 @@ class WP_REST_Post_Meta_Fields extends WP_REST_Meta_Fields { */ protected $post_type; + /** + * Cache invalidation callback for posts. + * + * @since 6.9.0 + * @var string + */ + protected $cache_callback = 'wp_cache_set_posts_last_changed'; + /** * Constructor. * diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-term-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-term-meta-fields.php index 281e1316c9ab0..4e969d80ddc19 100644 --- a/src/wp-includes/rest-api/fields/class-wp-rest-term-meta-fields.php +++ b/src/wp-includes/rest-api/fields/class-wp-rest-term-meta-fields.php @@ -24,6 +24,14 @@ class WP_REST_Term_Meta_Fields extends WP_REST_Meta_Fields { */ protected $taxonomy; + /** + * Cache invalidation callback for terms. + * + * @since 6.9.0 + * @var string + */ + protected $cache_callback = 'wp_cache_set_terms_last_changed'; + /** * Constructor. * diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-user-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-user-meta-fields.php index fb1513c58ad4c..6d05095c8aad7 100644 --- a/src/wp-includes/rest-api/fields/class-wp-rest-user-meta-fields.php +++ b/src/wp-includes/rest-api/fields/class-wp-rest-user-meta-fields.php @@ -16,6 +16,14 @@ */ class WP_REST_User_Meta_Fields extends WP_REST_Meta_Fields { + /** + * Cache invalidation callback for users. + * + * @since 6.9.0 + * @var string + */ + protected $cache_callback = 'wp_cache_set_users_last_changed'; + /** * Retrieves the user meta type. * diff --git a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php index 5ce72a57fa55f..3b4908558dd29 100644 --- a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php +++ b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php @@ -4014,4 +4014,68 @@ public static function data_scalar_default_values() { 'string default' => array( 'string', 'string', 'string2' ), ); } + + /** + * Tests that the cache callback is re-hooked after a batch meta update. + * + * @ticket 65486 + */ + public function test_update_value_batch_rehooks_cache_callback_after_update() { + register_meta( + 'post', + 'batch_key_1', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); + register_meta( + 'post', + 'batch_key_2', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); + register_meta( + 'post', + 'batch_key_3', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); + + global $wp_rest_server; + $wp_rest_server = new Spy_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + + $this->grant_write_permission(); + + $data = array( + 'meta' => array( + 'batch_key_1' => 'value1', + 'batch_key_2' => 'value2', + 'batch_key_3' => 'value3', + ), + ); + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( $data ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Verify meta values were persisted. + $this->assertSame( 'value1', get_post_meta( self::$post_id, 'batch_key_1', true ) ); + $this->assertSame( 'value2', get_post_meta( self::$post_id, 'batch_key_2', true ) ); + $this->assertSame( 'value3', get_post_meta( self::$post_id, 'batch_key_3', true ) ); + + // The cache callback must be re-hooked on all three meta actions. + $this->assertNotFalse( has_action( 'added_post_meta', 'wp_cache_set_posts_last_changed' ) ); + $this->assertNotFalse( has_action( 'updated_post_meta', 'wp_cache_set_posts_last_changed' ) ); + $this->assertNotFalse( has_action( 'deleted_post_meta', 'wp_cache_set_posts_last_changed' ) ); + } }