From 511202c7c22873271207944297545ebaadac9374 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Dec 2024 11:32:10 +0100 Subject: [PATCH 1/8] Cache `count_user_posts()` --- src/wp-includes/default-filters.php | 4 + src/wp-includes/user.php | 113 +++++++++++++++++--- tests/phpunit/tests/user.php | 16 +-- tests/phpunit/tests/user/countUserPosts.php | 10 +- 4 files changed, 118 insertions(+), 25 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index ae654605e8f4b..f006a16a4704c 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -116,6 +116,10 @@ add_action( $action, 'wp_maybe_update_user_counts', 10, 0 ); } +// User post counts. +add_action( 'post_updated', '_clear_user_posts_count_cache_on_author_change', 10, 3 ); +add_action( 'save_post', '_clear_user_posts_count_cache_on_update', 10, 2 ); + // Post meta. add_action( 'added_post_meta', 'wp_cache_set_posts_last_changed' ); add_action( 'updated_post_meta', 'wp_cache_set_posts_last_changed' ); diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index f60dbe5d3fae2..9be0759b5e409 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -566,6 +566,97 @@ function wp_validate_logged_in_cookie( $user_id ) { return wp_validate_auth_cookie( $_COOKIE[ LOGGED_IN_COOKIE ], 'logged_in' ); } +/** + * Counts the number of posts for a particular post type. + * + * @since 6.8.0 + * + * @param int $user_id User ID. + * @param string $post_type Optional. Post type to count the number of posts for. Default 'post'. + * @param bool $public_only Optional. Whether to only return counts for public posts. Default false. + * @return int Number of posts the user has written in this post type. + */ +function count_user_posts_for_post_type( $user_id, $post_type = 'post', $public_only = false ) { + global $wpdb; + + $where = get_posts_by_author_sql( $post_type, true, $user_id, $public_only ); + $where_hash = wp_hash( $where ); + $cache_key = "count_user_{$post_type}_{$user_id}_{$where_hash}"; + $cache_group = $public_only ? 'user_posts_count_public' : 'user_posts_count'; + + // Try to get count from cache. + $count = wp_cache_get( $cache_key, $cache_group ); + + // If cache is empty, query the database. + if ( false === $count ) { + $count = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" ); + + wp_cache_add( $cache_key, $count, $cache_group ); + } + + return absint( $count ); +} + +/** + * Clears the cached posts count for user for given post type. + * + * @since 6.8.0 + * + * @param int $user_id User ID. + * @param string $post_type Post type. + */ +function clear_user_posts_count_cache( $user_id, $post_type ) { + $cache_key = "count_user_{$post_type}_{$user_id}"; + $cache_groups = array( + 'user_posts_count_public', + 'user_posts_count', + ); + + foreach ( $cache_groups as $cache_group ) { + wp_cache_delete( $cache_key, $cache_group ); + } +} + +/** + * Clears the cached posts count for both users if a post's author changes. + * + * @since 6.8.0 + * @access private + * + * @param int $post_id Post ID. + * @param WP_Post $post_after Post object following the update. + * @param WP_Post $post_before Post object before the update. + */ +function _clear_user_posts_count_cache_on_author_change( $post_id, $post_after, $post_before ) { + if ( $post_after->post_author !== $post_before->post_author ) { + $post_type = get_post_type( $post_id ); + + clear_user_posts_count_cache( $post_after->post_author, $post_type ); + clear_user_posts_count_cache( $post_before->post_author, $post_type ); + } +} + +/** + * When the post is created, clear the cache. + * + * @since 6.8.0 + * @access private + * + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + */ +function _clear_user_posts_count_cache_on_update( $post_id, $post ) { + // Don't do anything if revision is being saved. + if ( wp_is_post_revision( $post_id ) ) { + return; + } + + $post_type = $post->post_type; + $author_id = $post->post_author; + + clear_user_posts_count_cache( $author_id, $post_type ); +} + /** * Gets the number of posts a user has written. * @@ -579,14 +670,18 @@ function wp_validate_logged_in_cookie( $user_id ) { * @param int $userid User ID. * @param array|string $post_type Optional. Single post type or array of post types to count the number of posts for. Default 'post'. * @param bool $public_only Optional. Whether to only return counts for public posts. Default false. - * @return string Number of posts the user has written in this post type. + * @return int Number of posts the user has written in this post type. */ function count_user_posts( $userid, $post_type = 'post', $public_only = false ) { - global $wpdb; + if ( is_string( $post_type ) ) { + $post_type = array( $post_type ); + } - $where = get_posts_by_author_sql( $post_type, true, $userid, $public_only ); + $count = 0; - $count = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" ); + foreach ( $post_type as $type ) { + $count += count_user_posts_for_post_type( $userid, $type, $public_only ); + } /** * Filters the number of posts a user has written. @@ -616,19 +711,13 @@ function count_user_posts( $userid, $post_type = 'post', $public_only = false ) * @return string[] Amount of posts each user has written, as strings, keyed by user ID. */ function count_many_users_posts( $users, $post_type = 'post', $public_only = false ) { - global $wpdb; - $count = array(); if ( empty( $users ) || ! is_array( $users ) ) { return $count; } - $userlist = implode( ',', array_map( 'absint', $users ) ); - $where = get_posts_by_author_sql( $post_type, true, null, $public_only ); - - $result = $wpdb->get_results( "SELECT post_author, COUNT(*) FROM $wpdb->posts $where AND post_author IN ($userlist) GROUP BY post_author", ARRAY_N ); - foreach ( $result as $row ) { - $count[ $row[0] ] = $row[1]; + foreach ( $users as $user_id ) { + $count[ $user_id ] = count_user_posts( $user_id, $post_type, $public_only ); } foreach ( $users as $id ) { diff --git a/tests/phpunit/tests/user.php b/tests/phpunit/tests/user.php index 884200530f1d9..ab86690acfd19 100644 --- a/tests/phpunit/tests/user.php +++ b/tests/phpunit/tests/user.php @@ -559,21 +559,21 @@ public function test_count_many_users_posts() { wp_set_current_user( self::$author_id ); $counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', false ); - $this->assertSame( '1', $counts[ self::$author_id ] ); - $this->assertSame( '1', $counts[ $user_id_b ] ); + $this->assertSame( 1, $counts[ self::$author_id ] ); + $this->assertSame( 1, $counts[ $user_id_b ] ); $counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', true ); - $this->assertSame( '1', $counts[ self::$author_id ] ); - $this->assertSame( '1', $counts[ $user_id_b ] ); + $this->assertSame( 1, $counts[ self::$author_id ] ); + $this->assertSame( 1, $counts[ $user_id_b ] ); wp_set_current_user( $user_id_b ); $counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', false ); - $this->assertSame( '1', $counts[ self::$author_id ] ); - $this->assertSame( '2', $counts[ $user_id_b ] ); + $this->assertSame( 1, $counts[ self::$author_id ] ); + $this->assertSame( 2, $counts[ $user_id_b ] ); $counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', true ); - $this->assertSame( '1', $counts[ self::$author_id ] ); - $this->assertSame( '1', $counts[ $user_id_b ] ); + $this->assertSame( 1, $counts[ self::$author_id ] ); + $this->assertSame( 1, $counts[ $user_id_b ] ); } /** diff --git a/tests/phpunit/tests/user/countUserPosts.php b/tests/phpunit/tests/user/countUserPosts.php index dbc94f418e7e6..ebf9722ab2dcb 100644 --- a/tests/phpunit/tests/user/countUserPosts.php +++ b/tests/phpunit/tests/user/countUserPosts.php @@ -59,34 +59,34 @@ public function set_up() { } public function test_count_user_posts_post_type_should_default_to_post() { - $this->assertSame( '4', count_user_posts( self::$user_id ) ); + $this->assertSame( 4, count_user_posts( self::$user_id ) ); } /** * @ticket 21364 */ public function test_count_user_posts_post_type_post() { - $this->assertSame( '4', count_user_posts( self::$user_id, 'post' ) ); + $this->assertSame( 4, count_user_posts( self::$user_id, 'post' ) ); } /** * @ticket 21364 */ public function test_count_user_posts_post_type_cpt() { - $this->assertSame( '3', count_user_posts( self::$user_id, 'wptests_pt' ) ); + $this->assertSame( 3, count_user_posts( self::$user_id, 'wptests_pt' ) ); } /** * @ticket 32243 */ public function test_count_user_posts_with_multiple_post_types() { - $this->assertSame( '7', count_user_posts( self::$user_id, array( 'wptests_pt', 'post' ) ) ); + $this->assertSame( 7, count_user_posts( self::$user_id, array( 'wptests_pt', 'post' ) ) ); } /** * @ticket 32243 */ public function test_count_user_posts_should_ignore_non_existent_post_types() { - $this->assertSame( '4', count_user_posts( self::$user_id, array( 'foo', 'post' ) ) ); + $this->assertSame( 4, count_user_posts( self::$user_id, array( 'foo', 'post' ) ) ); } } From 4713d3d148b3da01e5114e921a2c49a7e1bc9224 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Dec 2024 21:30:04 +0100 Subject: [PATCH 2/8] Only cache int values --- src/wp-includes/user.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 9be0759b5e409..48504702bbe94 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -589,12 +589,12 @@ function count_user_posts_for_post_type( $user_id, $post_type = 'post', $public_ // If cache is empty, query the database. if ( false === $count ) { - $count = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" ); + $count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" ) ); wp_cache_add( $cache_key, $count, $cache_group ); } - return absint( $count ); + return $count; } /** From 6c89dcf6dac5d12e99c11c83b76b0dac5eb9d324 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Dec 2024 21:30:13 +0100 Subject: [PATCH 3/8] Fix cache key --- src/wp-includes/user.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 48504702bbe94..f8411fbc1a9b7 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -580,8 +580,7 @@ function count_user_posts_for_post_type( $user_id, $post_type = 'post', $public_ global $wpdb; $where = get_posts_by_author_sql( $post_type, true, $user_id, $public_only ); - $where_hash = wp_hash( $where ); - $cache_key = "count_user_{$post_type}_{$user_id}_{$where_hash}"; + $cache_key = "count_user_{$post_type}_{$user_id}"; $cache_group = $public_only ? 'user_posts_count_public' : 'user_posts_count'; // Try to get count from cache. From a653465ed9dfc03a370a2eee6c3e2f0aa390bc74 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Dec 2024 21:50:47 +0100 Subject: [PATCH 4/8] More triggers --- src/wp-admin/includes/user.php | 4 ++++ src/wp-includes/default-filters.php | 2 +- src/wp-includes/post.php | 6 ++++++ src/wp-includes/user.php | 21 --------------------- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/wp-admin/includes/user.php b/src/wp-admin/includes/user.php index abed2d20157c5..1d6f73ea8c8db 100644 --- a/src/wp-admin/includes/user.php +++ b/src/wp-admin/includes/user.php @@ -445,6 +445,10 @@ function wp_delete_user( $id, $reassign = null ) { clean_user_cache( $user ); + foreach( get_post_types() as $post_type ) { + clear_user_posts_count_cache( $user->ID, $post_type ); + } + /** * Fires immediately after a user is deleted from the site. * diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index f006a16a4704c..bce9637c6191b 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -117,8 +117,8 @@ } // User post counts. +add_action( 'attachment_updated', '_clear_user_posts_count_cache_on_author_change', 10, 3 ); add_action( 'post_updated', '_clear_user_posts_count_cache_on_author_change', 10, 3 ); -add_action( 'save_post', '_clear_user_posts_count_cache_on_update', 10, 2 ); // Post meta. add_action( 'added_post_meta', 'wp_cache_set_posts_last_changed' ); diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index eb90e762f5663..c945abe8c43e9 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -3819,6 +3819,8 @@ function wp_delete_post( $post_id = 0, $force_delete = false ) { } } + clear_user_posts_count_cache( $post->post_author, $post->post_type ); + wp_clear_scheduled_hook( 'publish_future_post', array( $post_id ) ); /** @@ -5070,6 +5072,8 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true ) */ do_action( 'wp_insert_post', $post_id, $post, $update ); + clear_user_posts_count_cache( $post->post_author, $post_type ); + if ( $fire_after_hooks ) { wp_after_insert_post( $post, $update, $post_before ); } @@ -5236,6 +5240,8 @@ function wp_publish_post( $post ) { /** This action is documented in wp-includes/post.php */ do_action( 'wp_insert_post', $post->ID, $post, true ); + clear_user_posts_count_cache( $post->post_author, $post->post_type ); + wp_after_insert_post( $post, true, $post_before ); } diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index f8411fbc1a9b7..8c782975c3f14 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -635,27 +635,6 @@ function _clear_user_posts_count_cache_on_author_change( $post_id, $post_after, } } -/** - * When the post is created, clear the cache. - * - * @since 6.8.0 - * @access private - * - * @param int $post_id Post ID. - * @param WP_Post $post Post object. - */ -function _clear_user_posts_count_cache_on_update( $post_id, $post ) { - // Don't do anything if revision is being saved. - if ( wp_is_post_revision( $post_id ) ) { - return; - } - - $post_type = $post->post_type; - $author_id = $post->post_author; - - clear_user_posts_count_cache( $author_id, $post_type ); -} - /** * Gets the number of posts a user has written. * From 777de70ee0d3661c7f0e1897da1e37dc83a62cf2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Dec 2024 22:55:39 +0100 Subject: [PATCH 5/8] Lint fix --- src/wp-admin/includes/user.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/user.php b/src/wp-admin/includes/user.php index 1d6f73ea8c8db..8198bd464bb3d 100644 --- a/src/wp-admin/includes/user.php +++ b/src/wp-admin/includes/user.php @@ -445,7 +445,7 @@ function wp_delete_user( $id, $reassign = null ) { clean_user_cache( $user ); - foreach( get_post_types() as $post_type ) { + foreach ( get_post_types() as $post_type ) { clear_user_posts_count_cache( $user->ID, $post_type ); } From 674a4cb7daa625c4c4b4cad6900b74f587bfbcb1 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Dec 2024 23:24:16 +0100 Subject: [PATCH 6/8] Rename --- src/wp-includes/user.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 8c782975c3f14..b2b95cce020f7 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -645,12 +645,12 @@ function _clear_user_posts_count_cache_on_author_change( $post_id, $post_after, * * @global wpdb $wpdb WordPress database abstraction object. * - * @param int $userid User ID. + * @param int $user_id User ID. * @param array|string $post_type Optional. Single post type or array of post types to count the number of posts for. Default 'post'. * @param bool $public_only Optional. Whether to only return counts for public posts. Default false. * @return int Number of posts the user has written in this post type. */ -function count_user_posts( $userid, $post_type = 'post', $public_only = false ) { +function count_user_posts( $user_id, $post_type = 'post', $public_only = false ) { if ( is_string( $post_type ) ) { $post_type = array( $post_type ); } @@ -658,7 +658,7 @@ function count_user_posts( $userid, $post_type = 'post', $public_only = false ) $count = 0; foreach ( $post_type as $type ) { - $count += count_user_posts_for_post_type( $userid, $type, $public_only ); + $count += count_user_posts_for_post_type( $user_id, $type, $public_only ); } /** @@ -668,12 +668,12 @@ function count_user_posts( $userid, $post_type = 'post', $public_only = false ) * @since 4.1.0 Added `$post_type` argument. * @since 4.3.1 Added `$public_only` argument. * - * @param int $count The user's post count. - * @param int $userid User ID. - * @param string|array $post_type Single post type or array of post types to count the number of posts for. - * @param bool $public_only Whether to limit counted posts to public posts. + * @param int $count The user's post count. + * @param int $user_id User ID. + * @param array $post_type Array of post types to count the number of posts for. + * @param bool $public_only Whether to limit counted posts to public posts. */ - return apply_filters( 'get_usernumposts', $count, $userid, $post_type, $public_only ); + return apply_filters( 'get_usernumposts', $count, $user_id, $post_type, $public_only ); } /** From 708bdf2fd25a85f8ab6c0420980383bafc871066 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Dec 2024 23:27:24 +0100 Subject: [PATCH 7/8] Simplify --- src/wp-includes/user.php | 59 ++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index b2b95cce020f7..1b1d8ef7f6987 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -566,36 +566,6 @@ function wp_validate_logged_in_cookie( $user_id ) { return wp_validate_auth_cookie( $_COOKIE[ LOGGED_IN_COOKIE ], 'logged_in' ); } -/** - * Counts the number of posts for a particular post type. - * - * @since 6.8.0 - * - * @param int $user_id User ID. - * @param string $post_type Optional. Post type to count the number of posts for. Default 'post'. - * @param bool $public_only Optional. Whether to only return counts for public posts. Default false. - * @return int Number of posts the user has written in this post type. - */ -function count_user_posts_for_post_type( $user_id, $post_type = 'post', $public_only = false ) { - global $wpdb; - - $where = get_posts_by_author_sql( $post_type, true, $user_id, $public_only ); - $cache_key = "count_user_{$post_type}_{$user_id}"; - $cache_group = $public_only ? 'user_posts_count_public' : 'user_posts_count'; - - // Try to get count from cache. - $count = wp_cache_get( $cache_key, $cache_group ); - - // If cache is empty, query the database. - if ( false === $count ) { - $count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" ) ); - - wp_cache_add( $cache_key, $count, $cache_group ); - } - - return $count; -} - /** * Clears the cached posts count for user for given post type. * @@ -646,19 +616,36 @@ function _clear_user_posts_count_cache_on_author_change( $post_id, $post_after, * @global wpdb $wpdb WordPress database abstraction object. * * @param int $user_id User ID. - * @param array|string $post_type Optional. Single post type or array of post types to count the number of posts for. Default 'post'. + * @param array|string $post_types Optional. Single post type or array of post types to count the number of posts for. Default 'post'. * @param bool $public_only Optional. Whether to only return counts for public posts. Default false. * @return int Number of posts the user has written in this post type. */ -function count_user_posts( $user_id, $post_type = 'post', $public_only = false ) { - if ( is_string( $post_type ) ) { - $post_type = array( $post_type ); +function count_user_posts( $user_id, $post_types = 'post', $public_only = false ) { + global $wpdb; + + if ( is_string( $post_types ) ) { + $post_types = array( $post_types ); } + $cache_group = $public_only ? 'user_posts_count_public' : 'user_posts_count'; + $count = 0; - foreach ( $post_type as $type ) { - $count += count_user_posts_for_post_type( $user_id, $type, $public_only ); + foreach ( $post_types as $post_type ) { + $where = get_posts_by_author_sql( $post_type, true, $user_id, $public_only ); + $cache_key = "count_user_{$post_type}_{$user_id}"; + + // Try to get count from cache. + $post_type_count = wp_cache_get( $cache_key, $cache_group ); + + // If cache is empty, query the database. + if ( false === $post_type_count ) { + $post_type_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" ) ); + + wp_cache_add( $cache_key, $post_type_count, $cache_group ); + } + + $count += $post_type_count; } /** From d02f05c3ed8311d29519066ba15f01e077571341 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Dec 2024 23:36:44 +0100 Subject: [PATCH 8/8] Triggers --- src/wp-includes/post.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index c945abe8c43e9..51fa08361f04e 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -4985,6 +4985,8 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true ) do_action( 'add_attachment', $post_id ); } + clear_user_posts_count_cache( $post_author, $post_type ); + return $post_id; } @@ -5072,7 +5074,7 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true ) */ do_action( 'wp_insert_post', $post_id, $post, $update ); - clear_user_posts_count_cache( $post->post_author, $post_type ); + clear_user_posts_count_cache( $post_author, $post_type ); if ( $fire_after_hooks ) { wp_after_insert_post( $post, $update, $post_before );