Skip to content

Commit bb4d870

Browse files
committed
Users: Add caching to count_many_users_posts().
Introduces object caching to the `count_many_users_posts()` function. Argument equivalency is checked prior to generating the cache key to ensure that the same cache is hit regardless of array order for users and post types. For example `count_many_users_posts( [ 1, 2 ] )` will hit the same cache as `count_many_users_posts( [ 2, 1 ] )`. Props adamsilverstein, flixos90, kalpeshh, rollybueno, sachinrajcp123, shailu25, sirlouen, spacedmonkey, westonruter, wildworks. Fixes #63045. git-svn-id: https://develop.svn.wordpress.org/trunk@60941 602fd350-edb4-49c9-b593-d223f7449a82
1 parent b211ce0 commit bb4d870

File tree

2 files changed

+251
-7
lines changed

2 files changed

+251
-7
lines changed

src/wp-includes/user.php

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -649,13 +649,14 @@ function count_user_posts( $userid, $post_type = 'post', $public_only = false )
649649
* Gets the number of posts written by a list of users.
650650
*
651651
* @since 3.0.0
652+
* @since 6.9.0 The results are now cached.
652653
*
653654
* @global wpdb $wpdb WordPress database abstraction object.
654655
*
655656
* @param int[] $users Array of user IDs.
656657
* @param string|string[] $post_type Optional. Single post type or array of post types to check. Defaults to 'post'.
657658
* @param bool $public_only Optional. Only return counts for public posts. Defaults to false.
658-
* @return string[] Amount of posts each user has written, as strings, keyed by user ID.
659+
* @return array<int, string> Amount of posts each user has written, as strings, keyed by user ID.
659660
*/
660661
function count_many_users_posts( $users, $post_type = 'post', $public_only = false ) {
661662
global $wpdb;
@@ -682,14 +683,31 @@ function count_many_users_posts( $users, $post_type = 'post', $public_only = fal
682683
return $pre;
683684
}
684685

685-
$userlist = implode( ',', array_map( 'absint', $users ) );
686-
$where = get_posts_by_author_sql( $post_type, true, null, $public_only );
686+
// Cleanup the users array. Remove duplicates and sort for consistent ordering.
687+
$users = array_unique( array_filter( array_map( 'intval', $users ) ) );
688+
sort( $users );
687689

688-
$result = $wpdb->get_results( "SELECT post_author, COUNT(*) FROM $wpdb->posts $where AND post_author IN ($userlist) GROUP BY post_author", ARRAY_N );
690+
// Cleanup the post type argument. Remove duplicates and sort for consistent ordering.
691+
$post_type = array_unique( (array) $post_type );
692+
sort( $post_type );
693+
694+
$userlist = implode( ',', $users );
695+
$where = get_posts_by_author_sql( $post_type, true, null, $public_only );
696+
$query = "SELECT post_author, COUNT(*) FROM $wpdb->posts $where AND post_author IN ($userlist) GROUP BY post_author";
697+
$cache_key = 'count_many_users_posts:' . md5( $query );
698+
$cache_salts = array( wp_cache_get_last_changed( 'posts' ), wp_cache_get_last_changed( 'users' ) );
699+
$count = wp_cache_get_salted( $cache_key, 'post-queries', $cache_salts );
700+
701+
if ( false === $count ) {
702+
$where = get_posts_by_author_sql( $post_type, true, null, $public_only );
703+
$result = $wpdb->get_results( "SELECT post_author, COUNT(*) FROM $wpdb->posts $where AND post_author IN ($userlist) GROUP BY post_author", ARRAY_N );
704+
705+
$count = array_fill_keys( $users, 0 );
706+
foreach ( $result as $row ) {
707+
$count[ $row[0] ] = $row[1];
708+
}
689709

690-
$count = array_fill_keys( $users, 0 );
691-
foreach ( $result as $row ) {
692-
$count[ $row[0] ] = $row[1];
710+
wp_cache_set_salted( $cache_key, $count, 'post-queries', $cache_salts, HOUR_IN_SECONDS );
693711
}
694712

695713
return $count;

tests/phpunit/tests/user.php

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,8 @@ public function test_user_get_data_by_ID_should_alias_to_id() {
573573

574574
/**
575575
* @ticket 21431
576+
*
577+
* @covers ::count_many_users_posts
576578
*/
577579
public function test_count_many_users_posts() {
578580
$user_id_b = self::factory()->user->create( array( 'role' => 'author' ) );
@@ -604,6 +606,230 @@ public function test_count_many_users_posts() {
604606
$this->assertSame( '1', $counts[ $user_id_b ] );
605607
}
606608

609+
/**
610+
* Ensure the second and subsequent calls to count_many_users_posts() are cached.
611+
*
612+
* @ticket 63045
613+
*
614+
* @covers ::count_many_users_posts
615+
*/
616+
public function test_count_many_users_posts_is_cached() {
617+
$user_1 = self::$user_ids[0];
618+
$user_2 = self::$user_ids[1];
619+
620+
// Create posts for both users.
621+
self::factory()->post->create( array( 'post_author' => $user_1 ) );
622+
self::factory()->post->create( array( 'post_author' => $user_2 ) );
623+
624+
// Warm the cache.
625+
$count1 = count_many_users_posts( array( $user_1, $user_2 ), 'post', false );
626+
627+
// Ensure cache is hit for second call.
628+
$start_queries = get_num_queries();
629+
$count2 = count_many_users_posts( array( $user_1, $user_2 ), 'post', false );
630+
$end_queries = get_num_queries();
631+
$this->assertSame( 0, $end_queries - $start_queries, 'No database queries expected for second call to count_many_users_posts()' );
632+
$this->assertSameSetsWithIndex( $count1, $count2, 'Expected same results from both calls to count_many_users_posts()' );
633+
}
634+
635+
/**
636+
* Ensure equivalent arguments hit the same cache in count_many_users_posts().
637+
*
638+
* @ticket 63045
639+
*
640+
* @covers ::count_many_users_posts
641+
*
642+
* @dataProvider data_count_many_users_posts_cached_for_equivalent_arguments
643+
*
644+
* @param array $first_args First set of arguments to pass to count_many_users_posts().
645+
* @param array $second_args Second set of arguments to pass to count_many_users_posts().
646+
*/
647+
public function test_count_many_users_posts_cached_for_equivalent_arguments( $first_args, $second_args ) {
648+
// Replace placeholder user IDs with real ones.
649+
$first_args[0] = array_map(
650+
static function ( $user ) {
651+
return self::$user_ids[ $user ];
652+
},
653+
$first_args[0]
654+
);
655+
$second_args[0] = array_map(
656+
static function ( $user ) {
657+
return self::$user_ids[ $user ];
658+
},
659+
$second_args[0]
660+
);
661+
662+
// Warm the cache with the first set of arguments.
663+
$count1 = count_many_users_posts( ...$first_args );
664+
665+
// Ensure the cache is hit for the second set of equivalent arguments.
666+
$start_queries = get_num_queries();
667+
$count2 = count_many_users_posts( ...$second_args );
668+
$end_queries = get_num_queries();
669+
$this->assertSame( 0, $end_queries - $start_queries, 'No database queries expected for second call to count_many_users_posts() with equivalent arguments' );
670+
$this->assertSameSetsWithIndex( $count1, $count2, 'Expected same results from both calls to count_many_users_posts()' );
671+
}
672+
673+
/**
674+
* Data provider for test_count_many_users_posts_cached_for_equivalent_arguments().
675+
*
676+
* @return array[] Data provider.
677+
*/
678+
public function data_count_many_users_posts_cached_for_equivalent_arguments(): array {
679+
return array(
680+
'single post string vs array' => array(
681+
array( array( 0 ), 'post' ),
682+
array( array( 0 ), array( 'post' ) ),
683+
),
684+
'duplicate post type in array' => array(
685+
array( array( 0 ), array( 'post', 'post' ) ),
686+
array( array( 0 ), array( 'post' ) ),
687+
),
688+
'different post type order' => array(
689+
array( array( 0 ), array( 'post', 'page' ) ),
690+
array( array( 0 ), array( 'page', 'post' ) ),
691+
),
692+
'duplicate user IDs in array' => array(
693+
array( array( 0, 1, 1 ), 'post' ),
694+
array( array( 0, 1 ), 'post' ),
695+
),
696+
'different user order' => array(
697+
array( array( 0, 1 ), 'post' ),
698+
array( array( 1, 0 ), 'post' ),
699+
),
700+
'integer vs string user IDs' => array(
701+
array( array( 0, 1 ), 'post' ),
702+
array( array( '0', '1' ), 'post' ),
703+
),
704+
);
705+
}
706+
707+
/**
708+
* Test cache invalidation for count_many_users_posts().
709+
*
710+
* @ticket 63045
711+
*
712+
* @covers ::count_many_users_posts
713+
*/
714+
public function test_count_many_users_posts_cache_invalidation() {
715+
$user_1 = self::$user_ids[0];
716+
$user_2 = self::$user_ids[1];
717+
718+
// Create posts for both users.
719+
self::factory()->post->create( array( 'post_author' => $user_1 ) );
720+
self::factory()->post->create( array( 'post_author' => $user_2 ) );
721+
722+
$counts1 = count_many_users_posts( array( $user_1, $user_2 ), 'post', false );
723+
$this->assertSame(
724+
array(
725+
$user_1 => '1',
726+
$user_2 => '1',
727+
),
728+
$counts1,
729+
'Initial call is expected to have one post for each user.'
730+
);
731+
732+
// Create another post for user 1.
733+
self::factory()->post->create( array( 'post_author' => $user_1 ) );
734+
735+
$counts2 = count_many_users_posts( array( $user_1, $user_2 ), 'post', false );
736+
$this->assertSame(
737+
array(
738+
$user_1 => '2',
739+
$user_2 => '1',
740+
),
741+
$counts2,
742+
'Second call is expected to have two posts for user 1 and one post for user 2.'
743+
);
744+
}
745+
746+
/**
747+
* Ensure different post types use different caches in count_many_users_posts().
748+
*
749+
* @ticket 63045
750+
*
751+
* @covers ::count_many_users_posts
752+
*/
753+
public function test_different_post_types_use_different_caches() {
754+
$user_id = self::$user_ids[0];
755+
756+
// Create one post and two pages for the user.
757+
self::factory()->post->create(
758+
array(
759+
'post_author' => $user_id,
760+
'post_type' => 'post',
761+
)
762+
);
763+
self::factory()->post->create(
764+
array(
765+
'post_author' => $user_id,
766+
'post_type' => 'page',
767+
)
768+
);
769+
self::factory()->post->create(
770+
array(
771+
'post_author' => $user_id,
772+
'post_type' => 'page',
773+
)
774+
);
775+
776+
$start_queries = get_num_queries();
777+
$count1 = count_many_users_posts( array( $user_id ), 'post', false );
778+
$end_queries = get_num_queries();
779+
$this->assertSame( 1, $end_queries - $start_queries, 'Expected to hit database for first call to count_many_users_posts() with post type "post".' );
780+
$this->assertSame( '1', $count1[ $user_id ], 'Expected to have one post for user with post type "post".' );
781+
782+
$start_queries = get_num_queries();
783+
$count2 = count_many_users_posts( array( $user_id ), 'page', false );
784+
$end_queries = get_num_queries();
785+
$this->assertSame( 1, $end_queries - $start_queries, 'Expected to hit database for first call to count_many_users_posts() with post type "page".' );
786+
$this->assertSame( '2', $count2[ $user_id ], 'Expected to have two pages for user with post type "page".' );
787+
}
788+
789+
/**
790+
* Ensure different users use different caches in count_many_users_posts().
791+
*
792+
* @ticket 63045
793+
*
794+
* @covers ::count_many_users_posts
795+
*/
796+
public function test_different_users_use_different_caches() {
797+
$user_1 = self::$user_ids[0];
798+
$user_2 = self::$user_ids[1];
799+
800+
// Create one post for user 1, two for user 2.
801+
self::factory()->post->create(
802+
array(
803+
'post_author' => $user_1,
804+
'post_type' => 'post',
805+
)
806+
);
807+
self::factory()->post->create(
808+
array(
809+
'post_author' => $user_2,
810+
'post_type' => 'post',
811+
)
812+
);
813+
self::factory()->post->create(
814+
array(
815+
'post_author' => $user_2,
816+
'post_type' => 'post',
817+
)
818+
);
819+
820+
$start_queries = get_num_queries();
821+
$count1 = count_many_users_posts( array( $user_1 ), 'post', false );
822+
$end_queries = get_num_queries();
823+
$this->assertSame( 1, $end_queries - $start_queries, 'Expected to hit database for first call to count_many_users_posts() with user 1.' );
824+
$this->assertSame( '1', $count1[ $user_1 ], 'Expected to have one post for user 1 with post type "post".' );
825+
826+
$start_queries = get_num_queries();
827+
$count2 = count_many_users_posts( array( $user_2 ), 'post', false );
828+
$end_queries = get_num_queries();
829+
$this->assertSame( 1, $end_queries - $start_queries, 'Expected to hit database for first call to count_many_users_posts() with user 2.' );
830+
$this->assertSame( '2', $count2[ $user_2 ], 'Expected to have two posts for user 2 with post type "post".' );
831+
}
832+
607833
/**
608834
* @ticket 22858
609835
*/

0 commit comments

Comments
 (0)