diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index c7b924cd6f..ea43a695ed 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -2263,6 +2263,105 @@ class WP_Site_Health { return $result; } + /** + * Tests if sites uses persistent object cache. + * + * Checks if site uses persistent object cache or recommends to use it if not. + * + * @since 6.1.0 + * + * @return array The test result. + */ + public function get_test_persistent_object_cache() { + /** + * Filters the action URL for the persistent object cache health check. + * + * @since 6.1.0 + * + * @param string $action_url Learn more link for persistent object cache health check. + */ + $action_url = apply_filters( + 'site_status_persistent_object_cache_url', + /* translators: Localized Support reference. */ + __( 'https://wordpress.org/support/article/optimization/#object-caching' ) + ); + + $result = array( + 'test' => 'persistent_object_cache', + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Performance' ), + 'color' => 'blue', + ), + 'label' => __( 'A persistent object cache is being used' ), + 'description' => sprintf( + '

%s

', + __( "A persistent object cache makes your site's database more efficient, resulting in faster load times because WordPress can retrieve your site's content and settings much more quickly." ) + ), + 'actions' => sprintf( + '

%s %s

', + esc_url( $action_url ), + __( 'Learn more about persistent object caching.' ), + /* translators: Accessibility text. */ + __( '(opens in a new tab)' ) + ), + ); + + if ( wp_using_ext_object_cache() ) { + return $result; + } + + if ( ! $this->should_suggest_persistent_object_cache() ) { + $result['label'] = __( 'A persistent object cache is not required' ); + + return $result; + } + + $available_services = $this->available_object_cache_services(); + + $notes = __( 'Your hosting provider can tell you if a persistent object cache can be enabled on your site.' ); + + if ( ! empty( $available_services ) ) { + $notes .= ' ' . sprintf( + /* translators: Available object caching services. */ + __( 'Your host appears to support the following object caching services: %s.' ), + implode( ', ', $available_services ) + ); + } + + /** + * Filters the second paragraph of the health check's description + * when suggesting the use of a persistent object cache. + * + * Hosts may want to replace the notes to recommend their preferred object caching solution. + * + * Plugin authors may want to append notes (not replace) on why object caching is recommended for their plugin. + * + * @since 6.1.0 + * + * @param string $notes The notes appended to the health check description. + * @param array $available_services The list of available persistent object cache services. + */ + $notes = apply_filters( 'site_status_persistent_object_cache_notes', $notes, $available_services ); + + $result['status'] = 'recommended'; + $result['label'] = __( 'You should use a persistent object cache' ); + $result['description'] .= sprintf( + '

%s

', + wp_kses( + $notes, + array( + 'a' => array( 'href' => true ), + 'code' => true, + 'em' => true, + 'strong' => true, + ) + ) + ); + + return $result; + } + /** * Return a set of tests that belong to the site status page. * @@ -2383,6 +2482,14 @@ class WP_Site_Health { ); } + // Only check for a persistent object cache in production environments to not unnecessarily promote complicated setups. + if ( 'production' === wp_get_environment_type() ) { + $tests['direct']['persistent_object_cache'] = array( + 'label' => __( 'Persistent object cache' ), + 'test' => 'persistent_object_cache', + ); + } + /** * Add or modify which site status tests are run on a site. * @@ -2858,4 +2965,127 @@ class WP_Site_Health { return in_array( wp_get_environment_type(), array( 'development', 'local' ), true ); } + /** + * Determines whether to suggest using a persistent object cache. + * + * @since 6.1.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @return bool Whether to suggest using a persistent object cache. + */ + public function should_suggest_persistent_object_cache() { + global $wpdb; + + if ( is_multisite() ) { + return true; + } + + /** + * Filters whether to suggest use of a persistent object cache and bypass default threshold checks. + * + * Using this filter allows to override the default logic, effectively short-circuiting the method. + * + * @since 6.1.0 + * + * @param bool|null $suggest Boolean to short-circuit, for whether to suggest using a persistent object cache. + * Default null. + */ + $short_circuit = apply_filters( 'site_status_should_suggest_persistent_object_cache', null ); + if ( is_bool( $short_circuit ) ) { + return $short_circuit; + } + + /** + * Filters the thresholds used to determine whether to suggest the use of a persistent object cache. + * + * @since 6.1.0 + * + * @param array $thresholds The list of threshold names and numbers. + */ + $thresholds = apply_filters( + 'site_status_persistent_object_cache_thresholds', + array( + 'alloptions_count' => 500, + 'alloptions_bytes' => 100000, + 'comments_count' => 1000, + 'options_count' => 1000, + 'posts_count' => 1000, + 'terms_count' => 1000, + 'users_count' => 1000, + ) + ); + + $alloptions = wp_load_alloptions(); + + if ( $thresholds['alloptions_count'] < count( $alloptions ) ) { + return true; + } + + if ( $thresholds['alloptions_bytes'] < strlen( serialize( $alloptions ) ) ) { + return true; + } + + $table_names = implode( "','", array( $wpdb->comments, $wpdb->options, $wpdb->posts, $wpdb->terms, $wpdb->users ) ); + + // With InnoDB the `TABLE_ROWS` are estimates, which are accurate enough and faster to retrieve than individual `COUNT()` queries. + $results = $wpdb->get_results( + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- This query cannot use interpolation. + "SELECT TABLE_NAME AS 'table', TABLE_ROWS AS 'rows', SUM(data_length + index_length) as 'bytes' FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME IN ('$table_names') GROUP BY TABLE_NAME;", + DB_NAME + ), + OBJECT_K + ); + + $threshold_map = array( + 'comments_count' => $wpdb->comments, + 'options_count' => $wpdb->options, + 'posts_count' => $wpdb->posts, + 'terms_count' => $wpdb->terms, + 'users_count' => $wpdb->users, + ); + + foreach ( $threshold_map as $threshold => $table ) { + if ( $thresholds[ $threshold ] <= $results[ $table ]->rows ) { + return true; + } + } + + return false; + } + + /** + * Returns a list of available persistent object cache services. + * + * @since 6.1.0 + * + * @return array The list of available persistent object cache services. + */ + private function available_object_cache_services() { + $extensions = array_map( + 'extension_loaded', + array( + 'APCu' => 'apcu', + 'Redis' => 'redis', + 'Relay' => 'relay', + 'Memcache' => 'memcache', + 'Memcached' => 'memcached', + ) + ); + + $services = array_keys( array_filter( $extensions ) ); + + /** + * Filters the persistent object cache services available to the user. + * + * This can be useful to hide or add services not included in the defaults. + * + * @since 6.1.0 + * + * @param array $services The list of available persistent object cache services. + */ + return apply_filters( 'site_status_available_object_cache_services', $services ); + } + } diff --git a/tests/phpunit/tests/site-health.php b/tests/phpunit/tests/site-health.php index 956160361a..6325b54248 100644 --- a/tests/phpunit/tests/site-health.php +++ b/tests/phpunit/tests/site-health.php @@ -107,4 +107,75 @@ class Tests_Site_Health extends WP_UnitTestCase { ), ); } + + /** + * @group ms-excluded + * @ticket 56040 + */ + public function test_object_cache_default_thresholds() { + $wp_site_health = new WP_Site_Health(); + + $this->assertFalse( + $wp_site_health->should_suggest_persistent_object_cache() + ); + } + + + /** + * @group ms-required + * @ticket 56040 + */ + public function test_object_cache_default_thresholds_on_multisite() { + $wp_site_health = new WP_Site_Health(); + $this->assertTrue( + $wp_site_health->should_suggest_persistent_object_cache() + ); + } + + /** + * @ticket 56040 + */ + public function test_object_cache_thresholds_check_can_be_bypassed() { + $wp_site_health = new WP_Site_Health(); + add_filter( 'site_status_should_suggest_persistent_object_cache', '__return_true' ); + + $this->assertTrue( + $wp_site_health->should_suggest_persistent_object_cache() + ); + } + + /** + * @dataProvider thresholds + * @ticket 56040 + */ + public function test_object_cache_thresholds( $threshold, $count ) { + $wp_site_health = new WP_Site_Health(); + add_filter( + 'site_status_persistent_object_cache_thresholds', + function ( $thresholds ) use ( $threshold, $count ) { + return array_merge( $thresholds, array( $threshold => $count ) ); + } + ); + + $this->assertTrue( + $wp_site_health->should_suggest_persistent_object_cache() + ); + } + + /** + * Data provider. + * + * @ticket 56040 + */ + public function thresholds() { + return array( + array( 'comments_count', 0 ), + array( 'posts_count', 0 ), + array( 'terms_count', 1 ), + array( 'options_count', 100 ), + array( 'users_count', 0 ), + array( 'alloptions_count', 100 ), + array( 'alloptions_bytes', 1000 ), + ); + } }