diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 90710a5c1b2f4..9b052d5555f71 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -74,43 +74,35 @@ 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. * * @since 5.8.0 * @since 6.1.0 Added caching. + * @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 + * 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 ]; + $cache_group = 'theme_json_files'; + if ( $cache_key ) { + $decoded_file = wp_cache_get( $cache_key, $cache_group ); + if ( false !== $decoded_file ) { + return $decoded_file; + } } $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 ]; + if ( $cache_key ) { + wp_cache_set( $cache_key, $decoded_file, $cache_group ); + } + return $decoded_file; } } @@ -142,12 +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 ) { - $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); - static::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema; + // 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 ) { + $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 ); } /** @@ -162,7 +164,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 +249,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_json_data_for_theme( $wp_theme ); /** * Filters the data provided by the theme for global styles and settings. @@ -254,17 +261,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_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 ); /* * Merge the child theme.json into the parent theme.json. @@ -380,6 +385,45 @@ 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.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. + */ + 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 ) { + $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 `//`. * @@ -664,6 +708,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; @@ -677,7 +723,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 ); } /** diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index cc7cfa2f65238..064b45e86e3c6 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 ); @@ -768,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.' ); } /**