From 66b98b92c3493d7a1b0967afcc1fcf3d9c182efd Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 12 Feb 2025 21:48:04 +0000 Subject: [PATCH] Users: Add caching to count_user_posts function Introduced caching for the `count_user_posts` function to reduce redundant database queries. This ensures better performance by storing and reusing query results when possible. Additionally, sanitized and sorted the `$post_type` array to avoid invalid queries. Props spacedmonkey, peterwilsoncc, mamaduka, flixos90, johnjamesjacoby, swissspidy, dilip2615, johnregan3, wpgurudev, desrosj, milindmore22, Krstarica, dilipom13. Fixes #39242. git-svn-id: https://develop.svn.wordpress.org/trunk@59817 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/user.php | 14 +- tests/phpunit/tests/user/countUserPosts.php | 138 ++++++++++++++++++++ 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index ede1330251..748efa5181 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -604,9 +604,19 @@ function wp_validate_logged_in_cookie( $user_id ) { function count_user_posts( $userid, $post_type = 'post', $public_only = false ) { global $wpdb; - $where = get_posts_by_author_sql( $post_type, true, $userid, $public_only ); + $post_type = array_unique( (array) $post_type ); + sort( $post_type ); - $count = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" ); + $where = get_posts_by_author_sql( $post_type, true, $userid, $public_only ); + $query = "SELECT COUNT(*) FROM $wpdb->posts $where"; + + $last_changed = wp_cache_get_last_changed( 'posts' ); + $cache_key = 'count_user_posts:' . md5( $query ) . ':' . $last_changed; + $count = wp_cache_get( $cache_key, 'post-queries' ); + if ( false === $count ) { + $count = $wpdb->get_var( $query ); + wp_cache_set( $cache_key, $count, 'post-queries' ); + } /** * Filters the number of posts a user has written. diff --git a/tests/phpunit/tests/user/countUserPosts.php b/tests/phpunit/tests/user/countUserPosts.php index dbc94f418e..64b06adda2 100644 --- a/tests/phpunit/tests/user/countUserPosts.php +++ b/tests/phpunit/tests/user/countUserPosts.php @@ -89,4 +89,142 @@ class Tests_User_CountUserPosts extends WP_UnitTestCase { public function test_count_user_posts_should_ignore_non_existent_post_types() { $this->assertSame( '4', count_user_posts( self::$user_id, array( 'foo', 'post' ) ) ); } + + /** + * Post count be correct after reassigning posts to another user. + * + * @ticket 39242 + */ + public function test_reassigning_users_posts_modifies_count() { + // Create new user. + $new_user_id = self::factory()->user->create( + array( + 'role' => 'author', + ) + ); + + // Prior to reassigning posts. + $this->assertSame( '4', count_user_posts( self::$user_id ), 'Original user is expected to have a count of four posts prior to reassignment.' ); + $this->assertSame( '0', count_user_posts( $new_user_id ), 'New user is expected to have a count of zero posts prior to reassignment.' ); + + // Delete the original user, reassigning their posts to the new user. + wp_delete_user( self::$user_id, $new_user_id ); + + // After reassigning posts. + $this->assertSame( '0', count_user_posts( self::$user_id ), 'Original user is expected to have a count of zero posts following reassignment.' ); + $this->assertSame( '4', count_user_posts( $new_user_id ), 'New user is expected to have a count of four posts following reassignment.' ); + } + + /** + * Post count be correct after deleting user without reassigning posts. + * + * @ticket 39242 + */ + public function test_post_count_retained_after_deleting_user_without_reassigning_posts() { + $this->assertSame( '4', count_user_posts( self::$user_id ), 'User is expected to have a count of four posts prior to deletion.' ); + + // Delete the original user without reassigning their posts. + wp_delete_user( self::$user_id ); + + $this->assertSame( '0', count_user_posts( self::$user_id ), 'User is expected to have a count of zero posts following deletion.' ); + } + + /** + * Post count should work for users that don't exist but have posts assigned. + * + * @ticket 39242 + */ + public function test_count_user_posts_for_non_existent_user() { + $next_user_id = self::$user_id + 1; + + // Assign post to next user. + self::factory()->post->create( + array( + 'post_author' => $next_user_id, + 'post_type' => 'post', + ) + ); + + $next_user_post_count = count_user_posts( $next_user_id ); + $this->assertSame( '1', $next_user_post_count, 'Non-existent user is expected to have count of one post.' ); + } + + /** + * Cached user count value should be accurate after user is created. + * + * @ticket 39242 + */ + public function test_count_user_posts_for_user_created_after_being_assigned_posts() { + global $wpdb; + $next_user_id = (int) $wpdb->get_var( "SELECT `auto_increment` FROM INFORMATION_SCHEMA.TABLES WHERE table_name = '$wpdb->users'" ); + + // Assign post to next user. + self::factory()->post->create( + array( + 'post_author' => $next_user_id, + 'post_type' => 'post', + ) + ); + + // Cache the user count. + count_user_posts( $next_user_id ); + + // Create user. + $real_next_user_id = self::factory()->user->create( + array( + 'role' => 'author', + ) + ); + + $this->assertSame( $next_user_id, $real_next_user_id, 'User ID should match calculated value' ); + $this->assertSame( '1', count_user_posts( $next_user_id ), 'User is expected to have count of one post.' ); + } + + /** + * User count cache should be hit regardless of post type order. + * + * @ticket 39242 + */ + public function test_cache_should_be_hit_regardless_of_post_type_order() { + // Prime Cache. + count_user_posts( self::$user_id, array( 'wptests_pt', 'post' ) ); + + $query_num_start = get_num_queries(); + count_user_posts( self::$user_id, array( 'post', 'wptests_pt' ) ); + $total_queries = get_num_queries() - $query_num_start; + + $this->assertSame( 0, $total_queries, 'Cache should be hit regardless of post type order.' ); + } + + /** + * User count cache should be hit for string and array of post types. + * + * @ticket 39242 + */ + public function test_cache_should_be_hit_for_string_and_array_equivalent_queries() { + // Prime Cache. + count_user_posts( self::$user_id, 'post' ); + + $query_num_start = get_num_queries(); + count_user_posts( self::$user_id, array( 'post' ) ); + $total_queries = get_num_queries() - $query_num_start; + + $this->assertSame( 0, $total_queries, 'Cache should be hit for string and array equivalent post types.' ); + } + + /** + * User count cache should be hit for array duplicates and equivalent queries. + * + * @ticket 39242 + */ + public function test_cache_should_be_hit_for_and_array_duplicates_equivalent_queries() { + // Prime Cache + count_user_posts( self::$user_id, array( 'post', 'post', 'post' ) ); + + $query_num_start = get_num_queries(); + count_user_posts( self::$user_id, array( 'post' ) ); + $total_queries = get_num_queries() - $query_num_start; + + $this->assertSame( 0, $total_queries, 'Cache is expected to be hit for equivalent queries with duplicate post types' ); + } }