diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index b1a0d5198d67b..6b2695fe481d4 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -20,6 +20,19 @@ #[AllowDynamicProperties] class WP_Theme_JSON_Resolver { + /** + * Container for keep track of registered blocks. + * + * @since 6.1.0 + * @var array + */ + protected static $blocks_cache = array( + 'core' => array(), + 'blocks' => array(), + 'theme' => array(), + 'user' => array(), + ); + /** * Container for data coming from core. * @@ -28,6 +41,14 @@ class WP_Theme_JSON_Resolver { */ protected static $core = null; + /** + * Container for data coming from the blocks. + * + * @since 6.1.0 + * @var WP_Theme_JSON + */ + protected static $blocks = null; + /** * Container for data coming from the theme. * @@ -145,6 +166,10 @@ protected static function translate( $theme_json, $domain = 'default' ) { * @return WP_Theme_JSON Entity that holds core data. */ public static function get_core_data() { + if ( null !== static::$core && static::has_same_registered_blocks( 'core' ) ) { + return static::$core; + } + $config = static::read_json_file( __DIR__ . '/theme.json' ); $config = static::translate( $config ); @@ -162,6 +187,37 @@ public static function get_core_data() { return static::$core; } + /** + * Checks whether the registered blocks were already processed for this origin. + * + * @since 6.1.0 + * + * @param string $origin Data source for which to cache the blocks. + * Valid values are 'core', 'blocks', 'theme', and 'user'. + * @return bool True on success, false otherwise. + */ + protected static function has_same_registered_blocks( $origin ) { + // Bail out if the origin is invalid. + if ( ! isset( static::$blocks_cache[ $origin ] ) ) { + return false; + } + + $registry = WP_Block_Type_Registry::get_instance(); + $blocks = $registry->get_all_registered(); + + // Is there metadata for all currently registered blocks? + $block_diff = array_diff_key( $blocks, static::$blocks_cache[ $origin ] ); + if ( empty( $block_diff ) ) { + return true; + } + + foreach ( $blocks as $block_name => $block_type ) { + static::$blocks_cache[ $origin ][ $block_name ] = true; + } + + return false; + } + /** * Returns the theme's data. * @@ -189,19 +245,21 @@ public static function get_theme_data( $deprecated = array(), $options = array() $options = wp_parse_args( $options, array( 'with_supports' => true ) ); - $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) ); - $theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) ); + if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) { + $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) ); + $theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) ); - /** - * Filters the data provided by the theme for global styles and settings. - * - * @since 6.1.0 - * - * @param WP_Theme_JSON_Data Class to access and update the underlying data. - */ - $theme_json = apply_filters( 'theme_json_theme', new WP_Theme_JSON_Data( $theme_json_data, 'theme' ) ); - $theme_json_data = $theme_json->get_data(); - static::$theme = new WP_Theme_JSON( $theme_json_data ); + /** + * Filters the data provided by the theme for global styles and settings. + * + * @since 6.1.0 + * + * @param WP_Theme_JSON_Data Class to access and update the underlying data. + */ + $theme_json = apply_filters( 'theme_json_theme', new WP_Theme_JSON_Data( $theme_json_data, 'theme' ) ); + $theme_json_data = $theme_json->get_data(); + static::$theme = new WP_Theme_JSON( $theme_json_data ); + } if ( wp_get_theme()->parent() ) { // Get parent theme.json. @@ -258,7 +316,6 @@ public static function get_theme_data( $deprecated = array(), $options = array() } $with_theme_supports = new WP_Theme_JSON( $theme_support_data ); $with_theme_supports->merge( static::$theme ); - return $with_theme_supports; } @@ -272,7 +329,12 @@ public static function get_theme_data( $deprecated = array(), $options = array() public static function get_block_data() { $registry = WP_Block_Type_Registry::get_instance(); $blocks = $registry->get_all_registered(); - $config = array( 'version' => 2 ); + + if ( null !== static::$blocks && static::has_same_registered_blocks( 'blocks' ) ) { + return static::$blocks; + } + + $config = array( 'version' => 2 ); foreach ( $blocks as $block_name => $block_type ) { if ( isset( $block_type->supports['__experimentalStyle'] ) ) { $config['styles']['blocks'][ $block_name ] = static::remove_json_comments( $block_type->supports['__experimentalStyle'] ); @@ -298,7 +360,8 @@ public static function get_block_data() { $theme_json = apply_filters( 'theme_json_blocks', new WP_Theme_JSON_Data( $config, 'blocks' ) ); $config = $theme_json->get_data(); - return new WP_Theme_JSON( $config, 'blocks' ); + static::$blocks = new WP_Theme_JSON( $config, 'blocks' ); + return static::$blocks; } /** @@ -407,6 +470,10 @@ public static function get_user_data_from_wp_global_styles( $theme, $create_post * @return WP_Theme_JSON Entity that holds styles for user data. */ public static function get_user_data() { + if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) { + return static::$user; + } + $config = array(); $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() ); @@ -562,9 +629,18 @@ protected static function get_file_path_from_theme( $file_name, $template = fals * @since 5.8.0 * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`, * and `$i18n_schema` variables to reset. + * @since 6.1.0 Added the `$blocks` and `$blocks_cache` variables + * to reset. */ public static function clean_cached_data() { static::$core = null; + static::$blocks = null; + static::$blocks_cache = array( + 'core' => array(), + 'blocks' => array(), + 'theme' => array(), + 'user' => array(), + ); static::$theme = null; static::$user = null; static::$user_custom_post_type_id = null; diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index 9b43a4fb33eb1..bbfffde317b31 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -33,6 +33,52 @@ class Tests_Theme_wpThemeJsonResolver extends WP_UnitTestCase { */ private $queries = array(); + /** + * WP_Theme_JSON_Resolver::$blocks_cache property. + * + * @var ReflectionProperty + */ + private static $property_blocks_cache; + + /** + * Original value of the WP_Theme_JSON_Resolver::$blocks_cache property. + * + * @var array + */ + private static $property_blocks_cache_orig_value; + + /** + * WP_Theme_JSON_Resolver::$core property. + * + * @var ReflectionProperty + */ + private static $property_core; + + /** + * Original value of the WP_Theme_JSON_Resolver::$core property. + * + * @var WP_Theme_JSON + */ + private static $property_core_orig_value; + + public static function set_up_before_class() { + parent::set_up_before_class(); + + static::$property_blocks_cache = new ReflectionProperty( WP_Theme_JSON_Resolver::class, 'blocks_cache' ); + static::$property_blocks_cache->setAccessible( true ); + static::$property_blocks_cache_orig_value = static::$property_blocks_cache->getValue(); + + static::$property_core = new ReflectionProperty( WP_Theme_JSON_Resolver::class, 'core' ); + static::$property_core->setAccessible( true ); + static::$property_core_orig_value = static::$property_core->getValue(); + } + + public static function tear_down_after_class() { + static::$property_blocks_cache->setValue( WP_Theme_JSON_Resolver::class, static::$property_blocks_cache_orig_value ); + static::$property_core->setValue( WP_Theme_JSON_Resolver::class, static::$property_core_orig_value ); + parent::tear_down_after_class(); + } + public function set_up() { parent::set_up(); $this->theme_root = realpath( DIR_TESTDATA . '/themedir1' ); @@ -55,6 +101,9 @@ public function tear_down() { $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; wp_clean_themes_cache(); unset( $GLOBALS['wp_themes'] ); + + // Reset data between tests. + WP_Theme_JSON_Resolver::clean_cached_data(); parent::tear_down(); } @@ -190,6 +239,198 @@ public function test_translations_are_applied() { ); } + private function get_registered_block_names( $hard_reset = false ) { + static $expected_block_names; + + if ( ! $hard_reset && ! empty( $expected_block_names ) ) { + return $expected_block_names; + } + + $expected_block_names = array(); + $resolver = WP_Block_Type_Registry::get_instance(); + $blocks = $resolver->get_all_registered(); + foreach ( array_keys( $blocks ) as $block_name ) { + $expected_block_names[ $block_name ] = true; + } + + return $expected_block_names; + } + + /** + * Tests when WP_Theme_JSON_Resolver::$blocks_cache is empty or does not match + * the all registered blocks. + * + * Though this is a non-public method, it is vital to other functionality. + * Therefore, tests are provided to validate it functions as expected. + * + * @dataProvider data_has_same_registered_blocks_when_all_blocks_not_cached + * @ticket 56467 + * + * @param string $origin The origin to test. + */ + public function test_has_same_registered_blocks_when_all_blocks_not_cached( $origin, array $cache = array() ) { + $has_same_registered_blocks = new ReflectionMethod( WP_Theme_JSON_Resolver::class, 'has_same_registered_blocks' ); + $has_same_registered_blocks->setAccessible( true ); + $expected_cache = $this->get_registered_block_names(); + + // Set up the blocks cache for the origin. + $blocks_cache = static::$property_blocks_cache->getValue(); + $blocks_cache[ $origin ] = $cache; + static::$property_blocks_cache->setValue( null, $blocks_cache ); + + $this->assertFalse( $has_same_registered_blocks->invoke( null, $origin ), 'WP_Theme_JSON_Resolver::has_same_registered_blocks() should return false when same blocks are not cached' ); + $blocks_cache = static::$property_blocks_cache->getValue(); + $this->assertSameSets( $expected_cache, $blocks_cache[ $origin ], 'WP_Theme_JSON_Resolver::$blocks_cache should contain all expected block names for the given origin' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_has_same_registered_blocks_when_all_blocks_not_cached() { + return array( + 'origin: core; cache: empty' => array( + 'origin' => 'core', + ), + 'origin: blocks; cache: empty' => array( + 'origin' => 'blocks', + ), + 'origin: theme; cache: empty' => array( + 'origin' => 'theme', + ), + 'origin: user; cache: empty' => array( + 'origin' => 'user', + ), + 'origin: core; cache: not empty' => array( + 'origin' => 'core', + 'cache' => array( + 'core/block' => true, + ), + ), + 'origin: blocks; cache: not empty' => array( + 'origin' => 'blocks', + 'cache' => array( + 'core/block' => true, + 'core/comments' => true, + ), + ), + 'origin: theme; cache: not empty' => array( + 'origin' => 'theme', + 'cache' => array( + 'core/cover' => true, + ), + ), + 'origin: user; cache: not empty' => array( + 'origin' => 'user', + 'cache' => array( + 'core/gallery' => true, + ), + ), + ); + } + + /** + * Tests when WP_Theme_JSON_Resolver::$blocks_cache is empty or does not match + * the all registered blocks. + * + * Though this is a non-public method, it is vital to other functionality. + * Therefore, tests are provided to validate it functions as expected. + * + * @dataProvider data_has_same_registered_blocks_when_all_blocks_are_cached + * @ticket 56467 + * + * @param string $origin The origin to test. + */ + public function test_has_same_registered_blocks_when_all_blocks_are_cached( $origin ) { + $has_same_registered_blocks = new ReflectionMethod( WP_Theme_JSON_Resolver::class, 'has_same_registered_blocks' ); + $has_same_registered_blocks->setAccessible( true ); + $expected_cache = $this->get_registered_block_names(); + + // Set up the cache with all registered blocks. + $blocks_cache = static::$property_blocks_cache->getValue(); + $blocks_cache[ $origin ] = $this->get_registered_block_names(); + static::$property_blocks_cache->setValue( null, $blocks_cache ); + + $this->assertTrue( $has_same_registered_blocks->invoke( null, $origin ), 'WP_Theme_JSON_Resolver::has_same_registered_blocks() should return true when using the cache' ); + $this->assertSameSets( $expected_cache, $blocks_cache[ $origin ], 'WP_Theme_JSON_Resolver::$blocks_cache should contain all expected block names for the given origin' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_has_same_registered_blocks_when_all_blocks_are_cached() { + return array( + 'core' => array( 'core' ), + 'blocks' => array( 'blocks' ), + 'theme' => array( 'theme' ), + 'user' => array( 'user' ), + ); + } + + /** + * @dataProvider data_get_core_data + * @covers WP_Theme_JSON_Resolver::get_core_data + * @ticket 56467 + */ + public function test_get_core_data( $should_fire_filter, $core_is_cached, $blocks_are_cached ) { + WP_Theme_JSON_Resolver::clean_cached_data(); + + // If should cache core, then fire the method to cache it before running the tests. + if ( $core_is_cached ) { + WP_Theme_JSON_Resolver::get_core_data(); + } + + // If should cache registered blocks, then set them up before running the tests. + if ( $blocks_are_cached ) { + $blocks_cache = static::$property_blocks_cache->getValue(); + $blocks_cache['core'] = $this->get_registered_block_names(); + static::$property_blocks_cache->setValue( null, $blocks_cache ); + } + + $expected_filter_count = did_filter( 'theme_json_default' ); + $actual = WP_Theme_JSON_Resolver::get_core_data(); + if ( $should_fire_filter ) { + $expected_filter_count++; + } + + $this->assertSame( $expected_filter_count, did_filter( 'theme_json_default' ), 'The filter "theme_json_default" should fire the given number of times' ); + $this->assertInstanceOf( WP_Theme_JSON::class, $actual, 'WP_Theme_JSON_Resolver::get_core_data() should return instance of WP_Theme_JSON' ); + $this->assertSame( static::$property_core->getValue(), $actual, 'WP_Theme_JSON_Resolver::$core property should be the same object as returned from WP_Theme_JSON_Resolver::get_core_data()' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_core_data() { + return array( + 'When both caches are empty' => array( + 'should_fire_filter' => true, + 'core_is_cached' => false, + 'blocks_are_cached' => false, + ), + 'When the blocks_cache is not empty and matches' => array( + 'should_fire_filter' => true, + 'core_is_cached' => false, + 'blocks_are_cached' => true, + ), + 'When blocks_cache is empty but core cache is not' => array( + 'should_fire_filter' => true, + 'core_is_cached' => true, + 'blocks_are_cached' => false, + ), + 'When both caches are not empty' => array( + 'should_fire_filter' => true, + 'core_is_cached' => true, + 'blocks_are_cached' => false, + ), + ); + } + /** * @ticket 52991 */