From c54d91b72593d49c94aff7f7e7456c75330d4736 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 20 Sep 2023 15:46:05 -0500 Subject: [PATCH 1/9] Add persistent caching for theme.json file data and the i18n schema. --- .../class-wp-theme-json-resolver.php | 129 ++++++++++++++---- 1 file changed, 106 insertions(+), 23 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 50d6e09443b2a..b5e9699a84bb0 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -97,20 +97,39 @@ class WP_Theme_JSON_Resolver { * * @since 5.8.0 * @since 6.1.0 Added caching. + * @since 6.4.0 Added persistent caching using object cache. * * @param string $file_path Path to file. Empty if no file. + * @param string $cache_key Optional. Key to cache the result under. Omitting the parameter results in no caching + * being used. Default empty string. * @return array Contents that adhere to the theme.json schema. */ - protected static function read_json_file( $file_path ) { + protected static function read_json_file( $file_path, $cache_key = '' ) { if ( $file_path ) { if ( array_key_exists( $file_path, static::$theme_json_file_cache ) ) { return static::$theme_json_file_cache[ $file_path ]; } - $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) ); - if ( is_array( $decoded_file ) ) { - static::$theme_json_file_cache[ $file_path ] = $decoded_file; - return static::$theme_json_file_cache[ $file_path ]; + $cache_group = 'theme_json_files'; + if ( $cache_key ) { + $decoded_file = wp_cache_get( $cache_key, $cache_group ); + if ( false !== $decoded_file ) { + static::$theme_json_file_cache[ $file_path ] = $decoded_file; + return static::$theme_json_file_cache[ $file_path ]; + } + } + + if ( ! isset( static::$theme_json_file_cache[ $file_path ] ) ) { + $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) ); + if ( is_array( $decoded_file ) ) { + static::$theme_json_file_cache[ $file_path ] = $decoded_file; + + if ( $cache_key ) { + wp_cache_set( $cache_key, static::$theme_json_file_cache[ $file_path ], $cache_group ); + } + + return static::$theme_json_file_cache[ $file_path ]; + } } } @@ -143,8 +162,29 @@ public static function get_fields_to_translate() { */ protected static function translate( $theme_json, $domain = 'default' ) { if ( null === static::$i18n_schema ) { - $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); - static::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema; + // Only use cache when not currently developing for core. + $can_use_cached = ! wp_is_development_mode( 'core' ); + if ( $can_use_cached ) { + // Include an unmodified $wp_version. + require ABSPATH . WPINC . '/version.php'; + + $cache_group = 'theme_json_files'; + $cache_key = "i18n_schema_{$wp_version}"; + + $i18n_schema = wp_cache_get( $cache_key, $cache_group ); + if ( false !== $i18n_schema ) { + static::$i18n_schema = $i18n_schema; + } + } + + if ( null === static::$i18n_schema ) { + $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); + static::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema; + + if ( $can_use_cached ) { + wp_cache_set( $cache_key, static::$i18n_schema, $cache_group ); + } + } } return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain ); @@ -162,7 +202,16 @@ public static function get_core_data() { return static::$core; } - $config = static::read_json_file( __DIR__ . '/theme.json' ); + // Only use cache when not currently developing for core. + $cache_key = ''; + if ( ! wp_is_development_mode( 'core' ) ) { + // Include an unmodified $wp_version. + require ABSPATH . WPINC . '/version.php'; + + $cache_key = "core_{$wp_version}"; + } + + $config = static::read_json_file( __DIR__ . '/theme.json', $cache_key ); $config = static::translate( $config ); /** @@ -238,14 +287,10 @@ public static function get_theme_data( $deprecated = array(), $options = array() $options = wp_parse_args( $options, array( 'with_supports' => true ) ); if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) { - $wp_theme = wp_get_theme(); - $theme_json_file = $wp_theme->get_file_path( 'theme.json' ); - if ( is_readable( $theme_json_file ) ) { - $theme_json_data = static::read_json_file( $theme_json_file ); - $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) ); - } else { - $theme_json_data = array(); - } + $wp_theme = wp_get_theme(); + + // Read main theme.json (which may also come from the parent theme). + $raw_theme_json_data = static::read_theme_theme_json_data( $wp_theme ); /** * Filters the data provided by the theme for global styles and settings. @@ -254,17 +299,15 @@ public static function get_theme_data( $deprecated = array(), $options = array() * * @param WP_Theme_JSON_Data $theme_json Class to access and update the underlying data. */ - $theme_json = apply_filters( 'wp_theme_json_data_theme', new WP_Theme_JSON_Data( $theme_json_data, 'theme' ) ); + $theme_json = apply_filters( 'wp_theme_json_data_theme', new WP_Theme_JSON_Data( $raw_theme_json_data, 'theme' ) ); $theme_json_data = $theme_json->get_data(); static::$theme = new WP_Theme_JSON( $theme_json_data ); if ( $wp_theme->parent() ) { - // Get parent theme.json. - $parent_theme_json_file = $wp_theme->parent()->get_file_path( 'theme.json' ); - if ( $theme_json_file !== $parent_theme_json_file && is_readable( $parent_theme_json_file ) ) { - $parent_theme_json_data = static::read_json_file( $parent_theme_json_file ); - $parent_theme_json_data = static::translate( $parent_theme_json_data, $wp_theme->parent()->get( 'TextDomain' ) ); - $parent_theme = new WP_Theme_JSON( $parent_theme_json_data ); + // Read parent theme.json, and only merge it if successful and different from main theme.json data. + $raw_parent_theme_json_data = static::read_theme_theme_json_data( $wp_theme->parent() ); + if ( $raw_parent_theme_json_data && $raw_theme_json_data !== $raw_parent_theme_json_data ) { + $parent_theme = new WP_Theme_JSON( $raw_parent_theme_json_data ); /* * Merge the child theme.json into the parent theme.json. @@ -380,6 +423,46 @@ public static function get_block_data() { return static::$blocks; } + /** + * Returns theme.json data for the given theme. + * + * If the theme has a parent theme, the data may also come from the parent theme's theme.json. + * + * Data will be cached for the theme that the theme.json file belongs to. + * + * @since 6.4.0 + * + * @param WP_Theme $wp_theme Theme instance. + * @return array Raw array of data read from theme.json, or empty array if not readable. + */ + protected static function read_theme_json_data_for_theme( $wp_theme ) { + $theme_json_file = $wp_theme->get_file_path( 'theme.json' ); + + if ( ! is_readable( $theme_json_file ) ) { + return array(); + } + + // If the file found is actually from the parent theme, cache it for the parent theme instead. + if ( $wp_theme->parent() ) { + $parent_theme_json_file = $wp_theme->parent()->get_file_path( 'theme.json' ); + if ( $theme_json_file === $parent_theme_json_file ) { + $theme_json_file = $parent_theme_json_file; + $wp_theme = $wp_theme->parent(); + } + } + + // Only use cache when not currently developing the theme. + $cache_key = ''; + if ( ! wp_is_development_mode( 'theme' ) ) { + $cache_key = "theme_{$wp_theme->stylesheet}_{$wp_theme->version}"; + } + + $theme_json_data = static::read_json_file( $theme_json_file, $cache_key ); + $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) ); + + return $theme_json_data; + } + /** * When given an array, this will remove any keys with the name `//`. * From fb227c70b201f58ca24424b078184af22eac8b26 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 20 Sep 2023 15:55:22 -0500 Subject: [PATCH 2/9] Fix typo. --- src/wp-includes/class-wp-theme-json-resolver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index b5e9699a84bb0..ce989189f7eb0 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -290,7 +290,7 @@ public static function get_theme_data( $deprecated = array(), $options = array() $wp_theme = wp_get_theme(); // Read main theme.json (which may also come from the parent theme). - $raw_theme_json_data = static::read_theme_theme_json_data( $wp_theme ); + $raw_theme_json_data = static::read_theme_json_data_for_theme( $wp_theme ); /** * Filters the data provided by the theme for global styles and settings. @@ -305,7 +305,7 @@ public static function get_theme_data( $deprecated = array(), $options = array() if ( $wp_theme->parent() ) { // Read parent theme.json, and only merge it if successful and different from main theme.json data. - $raw_parent_theme_json_data = static::read_theme_theme_json_data( $wp_theme->parent() ); + $raw_parent_theme_json_data = static::read_theme_json_data_for_theme( $wp_theme->parent() ); if ( $raw_parent_theme_json_data && $raw_theme_json_data !== $raw_parent_theme_json_data ) { $parent_theme = new WP_Theme_JSON( $raw_parent_theme_json_data ); From a7d99d78992e370ab130f1cec79475301909d161 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 13 Nov 2023 14:31:26 -0800 Subject: [PATCH 3/9] Remove now useless internal cache properties as they are replaced with object cache. --- .../class-wp-theme-json-resolver.php | 84 ++++++------------- 1 file changed, 27 insertions(+), 57 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 13df675e07008..5a8b2ad48bb73 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -74,23 +74,6 @@ class WP_Theme_JSON_Resolver { */ protected static $user_custom_post_type_id = null; - /** - * Container to keep loaded i18n schema for `theme.json`. - * - * @since 5.8.0 As `$theme_json_i18n`. - * @since 5.9.0 Renamed from `$theme_json_i18n` to `$i18n_schema`. - * @var array - */ - protected static $i18n_schema = null; - - /** - * `theme.json` file cache. - * - * @since 6.1.0 - * @var array - */ - protected static $theme_json_file_cache = array(); - /** * Processes a file that adheres to the theme.json schema * and returns an array with its contents, or a void array if none found. @@ -106,30 +89,20 @@ class WP_Theme_JSON_Resolver { */ protected static function read_json_file( $file_path, $cache_key = '' ) { if ( $file_path ) { - if ( array_key_exists( $file_path, static::$theme_json_file_cache ) ) { - return static::$theme_json_file_cache[ $file_path ]; - } - $cache_group = 'theme_json_files'; if ( $cache_key ) { $decoded_file = wp_cache_get( $cache_key, $cache_group ); if ( false !== $decoded_file ) { - static::$theme_json_file_cache[ $file_path ] = $decoded_file; - return static::$theme_json_file_cache[ $file_path ]; + return $decoded_file; } } - if ( ! isset( static::$theme_json_file_cache[ $file_path ] ) ) { - $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) ); - if ( is_array( $decoded_file ) ) { - static::$theme_json_file_cache[ $file_path ] = $decoded_file; - - if ( $cache_key ) { - wp_cache_set( $cache_key, static::$theme_json_file_cache[ $file_path ], $cache_group ); - } - - return static::$theme_json_file_cache[ $file_path ]; + $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) ); + if ( is_array( $decoded_file ) ) { + if ( $cache_key ) { + wp_cache_set( $cache_key, $decoded_file, $cache_group ); } + return $decoded_file; } } @@ -161,33 +134,22 @@ public static function get_fields_to_translate() { * @return array Returns the modified $theme_json_structure. */ protected static function translate( $theme_json, $domain = 'default' ) { - if ( null === static::$i18n_schema ) { - // Only use cache when not currently developing for core. - $can_use_cached = ! wp_is_development_mode( 'core' ); - if ( $can_use_cached ) { - // Include an unmodified $wp_version. - require ABSPATH . WPINC . '/version.php'; - - $cache_group = 'theme_json_files'; - $cache_key = "i18n_schema_{$wp_version}"; - - $i18n_schema = wp_cache_get( $cache_key, $cache_group ); - if ( false !== $i18n_schema ) { - static::$i18n_schema = $i18n_schema; - } - } + // Include an unmodified $wp_version. + require ABSPATH . WPINC . '/version.php'; - if ( null === static::$i18n_schema ) { - $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); - static::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema; + $cache_group = 'theme_json_files'; + $cache_key = "i18n_schema_{$wp_version}"; - if ( $can_use_cached ) { - wp_cache_set( $cache_key, static::$i18n_schema, $cache_group ); - } - } + $i18n_schema = wp_cache_get( $cache_key, $cache_group ); + + if ( false === $i18n_schema ) { + $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); + $i18n_schema = null === $i18n_schema ? array() : $i18n_schema; + + wp_cache_set( $cache_key, $i18n_schema, $cache_group ); } - return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain ); + return translate_settings_using_i18n_schema( $i18n_schema, $theme_json, $domain ); } /** @@ -747,6 +709,8 @@ protected static function get_file_path_from_theme( $file_name, $template = fals * and `$i18n_schema` variables to reset. * @since 6.1.0 Added the `$blocks` and `$blocks_cache` variables * to reset. + * @since 6.5.0 Modified resetting of i18n schema to use cache + * instead of variable. */ public static function clean_cached_data() { static::$core = null; @@ -760,7 +724,13 @@ public static function clean_cached_data() { static::$theme = null; static::$user = null; static::$user_custom_post_type_id = null; - static::$i18n_schema = null; + + // Include an unmodified $wp_version. + require ABSPATH . WPINC . '/version.php'; + + $cache_group = 'theme_json_files'; + $cache_key = "i18n_schema_{$wp_version}"; + wp_cache_delete( $cache_key, $cache_group ); } /** From 9e2e707b55e31137c4e2f8e5c224dfca0dfa36ba Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 13 Nov 2023 14:41:17 -0800 Subject: [PATCH 4/9] Fix test accessing now removed property (reset no longer necessary as object cache is flushed between tests anyways). --- tests/phpunit/tests/theme/wpThemeJsonResolver.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index cc7cfa2f65238..de01170265821 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -756,11 +756,6 @@ public function test_get_theme_data_does_not_parse_theme_json_if_not_present() { $theme_json_resolver = new WP_Theme_JSON_Resolver(); - // Force-unset $i18n_schema property to "unload" translation schema. - $property = new ReflectionProperty( $theme_json_resolver, 'i18n_schema' ); - $property->setAccessible( true ); - $property->setValue( null, null ); - // A completely empty theme.json data set still has the 'version' key when parsed. $empty_theme_json = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ); From 551d4dcffe4e30231c6ccc988c7280cb1289d27a Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 13 Nov 2023 14:43:37 -0800 Subject: [PATCH 5/9] Update since annotations. --- src/wp-includes/class-wp-theme-json-resolver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 5a8b2ad48bb73..c87afa60eeae0 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -80,7 +80,7 @@ class WP_Theme_JSON_Resolver { * * @since 5.8.0 * @since 6.1.0 Added caching. - * @since 6.4.0 Added persistent caching using object cache. + * @since 6.5.0 Added persistent caching using object cache. * * @param string $file_path Path to file. Empty if no file. * @param string $cache_key Optional. Key to cache the result under. Omitting the parameter results in no caching @@ -392,7 +392,7 @@ public static function get_block_data() { * * Data will be cached for the theme that the theme.json file belongs to. * - * @since 6.4.0 + * @since 6.5.0 * * @param WP_Theme $wp_theme Theme instance. * @return array Raw array of data read from theme.json, or empty array if not readable. From 40e111af19be4d371ebb1535e8a4c097a7c22ac4 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 13 Nov 2023 14:55:25 -0800 Subject: [PATCH 6/9] Fix test to now rely on object cache API. --- tests/phpunit/tests/theme/wpThemeJsonResolver.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index de01170265821..35ac9f296dea7 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -763,7 +763,12 @@ public function test_get_theme_data_does_not_parse_theme_json_if_not_present() { $theme_data = $theme_json_resolver->get_theme_data( array(), array( 'with_supports' => false ) ); $this->assertInstanceOf( 'WP_Theme_JSON', $theme_data, 'Theme data should be an instance of WP_Theme_JSON.' ); $this->assertSame( $empty_theme_json, $theme_data->get_raw_data(), 'Theme data should be empty without theme support.' ); - $this->assertNull( $property->getValue(), 'Theme i18n schema should not have been loaded without theme support.' ); + + // Include an unmodified $wp_version. + require ABSPATH . WPINC . '/version.php'; + $cache_group = 'theme_json_files'; + $cache_key = "i18n_schema_{$wp_version}"; + $this->assertFalse( wp_cache_get( $cache_key, $cache_group ), 'Theme i18n schema should not have been loaded without theme support.' ); } /** From 5d8efc89499e23c28a1c03bb4595529ca7e8a458 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 13 Nov 2023 14:56:24 -0800 Subject: [PATCH 7/9] Remove useless variable assignment. --- src/wp-includes/class-wp-theme-json-resolver.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index c87afa60eeae0..d6f9da36f9a8f 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -408,8 +408,7 @@ protected static function read_theme_json_data_for_theme( $wp_theme ) { if ( $wp_theme->parent() ) { $parent_theme_json_file = $wp_theme->parent()->get_file_path( 'theme.json' ); if ( $theme_json_file === $parent_theme_json_file ) { - $theme_json_file = $parent_theme_json_file; - $wp_theme = $wp_theme->parent(); + $wp_theme = $wp_theme->parent(); } } From bb31313c1436809c3c43e3f5a79ca2cb7d77cc83 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 13 Nov 2023 14:59:07 -0800 Subject: [PATCH 8/9] Fix WPCS. --- tests/phpunit/tests/theme/wpThemeJsonResolver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index 35ac9f296dea7..064b45e86e3c6 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -766,8 +766,8 @@ public function test_get_theme_data_does_not_parse_theme_json_if_not_present() { // Include an unmodified $wp_version. require ABSPATH . WPINC . '/version.php'; - $cache_group = 'theme_json_files'; - $cache_key = "i18n_schema_{$wp_version}"; + $cache_group = 'theme_json_files'; + $cache_key = "i18n_schema_{$wp_version}"; $this->assertFalse( wp_cache_get( $cache_key, $cache_group ), 'Theme i18n schema should not have been loaded without theme support.' ); } From d990cfa8fd99c5df37c27a9ae5593fbb06b7421f Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 13 Nov 2023 16:31:27 -0800 Subject: [PATCH 9/9] Fix incorrect indentation. --- src/wp-includes/class-wp-theme-json-resolver.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index d6f9da36f9a8f..9b052d5555f71 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -137,8 +137,8 @@ protected static function translate( $theme_json, $domain = 'default' ) { // Include an unmodified $wp_version. require ABSPATH . WPINC . '/version.php'; - $cache_group = 'theme_json_files'; - $cache_key = "i18n_schema_{$wp_version}"; + $cache_group = 'theme_json_files'; + $cache_key = "i18n_schema_{$wp_version}"; $i18n_schema = wp_cache_get( $cache_key, $cache_group ); @@ -727,8 +727,8 @@ public static function clean_cached_data() { // Include an unmodified $wp_version. require ABSPATH . WPINC . '/version.php'; - $cache_group = 'theme_json_files'; - $cache_key = "i18n_schema_{$wp_version}"; + $cache_group = 'theme_json_files'; + $cache_key = "i18n_schema_{$wp_version}"; wp_cache_delete( $cache_key, $cache_group ); }