Make WordPress Core

Changeset 60941


Ignore:
Timestamp:
10/15/2025 11:56:57 PM (11 months ago)
Author:
peterwilsoncc
Message:

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.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/user.php

    r60712 r60941  
    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.
     
    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 */
    660661function count_many_users_posts( $users, $post_type = 'post', $public_only = false ) {
     
    683684        }
    684685
    685         $userlist = implode( ',', array_map( 'absint', $users ) );
    686         $where    = get_posts_by_author_sql( $post_type, true, null, $public_only );
    687 
    688         $result = $wpdb->get_results( "SELECT post_author, COUNT(*) FROM $wpdb->posts $where AND post_author IN ($userlist) GROUP BY post_author", ARRAY_N );
    689 
    690         $count = array_fill_keys( $users, 0 );
    691         foreach ( $result as $row ) {
    692                 $count[ $row[0] ] = $row[1];
     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 );
     689
     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                }
     709
     710                wp_cache_set_salted( $cache_key, $count, 'post-queries', $cache_salts, HOUR_IN_SECONDS );
    693711        }
    694712
  • trunk/tests/phpunit/tests/user.php

    r60634 r60941  
    574574        /**
    575575         * @ticket 21431
     576         *
     577         * @covers ::count_many_users_posts
    576578         */
    577579        public function test_count_many_users_posts() {
     
    603605                $this->assertSame( '1', $counts[ self::$author_id ] );
    604606                $this->assertSame( '1', $counts[ $user_id_b ] );
     607        }
     608
     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".' );
    605831        }
    606832
Note: See TracChangeset for help on using the changeset viewer.