Make WordPress Core

Changeset 60941


Ignore:
Timestamp:
10/15/2025 11:56:57 PM (2 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.