Ticket #62002: add-metadata-registry-v4.diff
File add-metadata-registry-v4.diff, 15.0 KB (added by , 5 months ago) |
---|
-
src/wp-includes/blocks.php
diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index a53092e04e..3f4f939c79 100644
a b function get_block_metadata_i18n_schema() { 375 375 return $i18n_block_schema; 376 376 } 377 377 378 /** 379 * Registers a block metadata collection. 380 * 381 * This function allows core and third-party plugins to register their block metadata 382 * collections in a centralized location. Registering collections can improve performance 383 * by avoiding multiple reads from the filesystem. 384 * 385 * @since 6.X.0 386 * 387 * @param string $path The base path for the collection. 388 * @param string $manifest The path to the manifest file for the collection. 389 */ 390 function wp_register_block_metadata_collection( $path, $manifest ) { 391 WP_Block_Metadata_Registry::register_collection( $path, $manifest ); 392 } 393 378 394 /** 379 395 * Registers a block type from the metadata stored in the `block.json` file. 380 396 * … … function register_block_type_from_metadata( $file_or_folder, $args = array() ) { 402 418 * instead of reading a JSON file per-block, and then decoding from JSON to PHP. 403 419 * Using a static variable ensures that the metadata is only read once per request. 404 420 */ 405 static $core_blocks_meta;406 if ( ! $core_blocks_meta ) {407 $core_blocks_meta = require ABSPATH . WPINC . '/blocks/blocks-json.php';408 }409 421 410 422 $metadata_file = ( ! str_ends_with( $file_or_folder, 'block.json' ) ) ? 411 423 trailingslashit( $file_or_folder ) . 'block.json' : 412 424 $file_or_folder; 413 425 414 $ is_core_block = str_starts_with( $file_or_folder, ABSPATH . WPINC);415 // If the block is not a core block, the metadata file must exist.416 $metadata_file_exists = $ is_core_block|| file_exists( $metadata_file );426 $maybe_metadata_from_registry = WP_Block_Metadata_Registry::get_metadata( $file_or_folder ); 427 // If the block is not registered in the metadata registry, the metadata file must exist. 428 $metadata_file_exists = $maybe_metadata_from_registry || file_exists( $metadata_file ); 417 429 if ( ! $metadata_file_exists && empty( $args['name'] ) ) { 418 430 return false; 419 431 } 420 432 421 433 // Try to get metadata from the static cache for core blocks. 422 434 $metadata = array(); 423 if ( $is_core_block ) { 424 $core_block_name = str_replace( ABSPATH . WPINC . '/blocks/', '', $file_or_folder ); 425 if ( ! empty( $core_blocks_meta[ $core_block_name ] ) ) { 426 $metadata = $core_blocks_meta[ $core_block_name ]; 427 } 435 if ( $maybe_metadata_from_registry ) { 436 $metadata = $maybe_metadata_from_registry; 428 437 } 429 438 430 439 // If metadata is not found in the static cache, read it from the file. -
src/wp-includes/blocks/index.php
diff --git a/src/wp-includes/blocks/index.php b/src/wp-includes/blocks/index.php index 40967727da..6582d07e9e 100644
a b function register_core_block_types_from_metadata() { 155 155 } 156 156 } 157 157 add_action( 'init', 'register_core_block_types_from_metadata' ); 158 159 /** 160 * Registers the core block metadata collection. 161 * 162 * This function is hooked into the 'init' action with a priority of 9, 163 * ensuring that the core block metadata is registered before the regular 164 * block initialization that happens at priority 10. 165 * 166 * @since 6.X.0 167 */ 168 function wp_register_core_block_metadata_collection() { 169 wp_register_block_metadata_collection( 170 BLOCKS_PATH, 171 BLOCKS_PATH . 'blocks-json.php' 172 ); 173 } 174 add_action( 'init', 'wp_register_core_block_metadata_collection', 9 ); -
new file src/wp-includes/class-wp-block-metadata-registry.php
diff --git a/src/wp-includes/class-wp-block-metadata-registry.php b/src/wp-includes/class-wp-block-metadata-registry.php new file mode 100644 index 0000000000..b1b2536d9a
- + 1 <?php 2 /** 3 * Block Metadata Registry 4 * 5 * @package WordPress 6 * @subpackage Blocks 7 * @since 6.X.0 8 */ 9 10 /** 11 * Class used for managing block metadata collections. 12 * 13 * The WP_Block_Metadata_Registry allows plugins to register metadata for large 14 * collections of blocks (e.g., 50-100+) using a single PHP file. This approach 15 * reduces the need to read and decode multiple `block.json` files, enhancing 16 * performance through opcode caching. 17 * 18 * @since 6.X.0 19 */ 20 class WP_Block_Metadata_Registry { 21 22 /** 23 * Container for storing block metadata collections. 24 * 25 * Each entry maps a base path to its corresponding metadata and callback. 26 * 27 * @since 6.X.0 28 * @var array<string, array<string, mixed>> 29 */ 30 private static $collections = array(); 31 32 /** 33 * Caches the last matched collection path for performance optimization. 34 * 35 * @since 6.X.0 36 * @var string|null 37 */ 38 private static $last_matched_collection = null; 39 40 /** 41 * Registers a block metadata collection. 42 * 43 * This method allows registering a collection of block metadata from a single 44 * manifest file, improving performance for large sets of blocks. 45 * 46 * @since 6.X.0 47 * 48 * @param string $path The absolute base path for the collection ( e.g., WP_PLUGIN_DIR . '/my-plugin/blocks/' ). 49 * @param string $manifest The absolute path to the manifest file containing the metadata collection. 50 * @param callable $identifier_callback Optional. Callback to determine the block identifier from a path. 51 * The callback should accept a string (file or folder path) and return a string (block identifier). 52 * This allows custom mapping between file paths and block names in the manifest. 53 * If null, the default identifier callback is used, which extracts the parent 54 * directory name. For example, when calling get_metadata() with a path like 55 * 'WP_PLUGIN_DIR/my-plugin/blocks/example/block.json', it would look for 56 * a key named "example" in the manifest. 57 * @return bool True if the collection was registered successfully, false otherwise. 58 */ 59 public static function register_collection( $path, $manifest, $identifier_callback = null ) { 60 $path = wp_normalize_path( rtrim( $path, '/' ) ); 61 62 // Check if the path is valid: 63 if ( str_starts_with( $path, wp_normalize_path( ABSPATH . WPINC ) ) ) { 64 // Core path is valid. 65 } elseif ( str_starts_with( $path, wp_normalize_path( WP_PLUGIN_DIR ) ) ) { 66 // For plugins, ensure the path is within a specific plugin directory and not the base plugin directory. 67 $plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ); 68 $relative_path = substr( $path, strlen( $plugin_dir ) + 1 ); 69 $plugin_name = strtok( $relative_path, '/' ); 70 71 if ( empty( $plugin_name ) || $plugin_name === $relative_path ) { 72 // Invalid plugin path. 73 return false; 74 } 75 } else { 76 // Path is neither core nor a valid plugin path. 77 return false; 78 } 79 80 self::$collections[ $path ] = array( 81 'manifest' => $manifest, 82 'metadata' => null, 83 'identifier_callback' => $identifier_callback, 84 ); 85 86 return true; 87 } 88 89 /** 90 * Retrieves block metadata for a given block within a specific collection. 91 * 92 * This method uses the registered collections to efficiently lookup 93 * block metadata without reading individual `block.json` files. 94 * 95 * @since 6.X.0 96 * 97 * @param string $file_or_folder The path to the file or folder containing the block. 98 * @return array|null The block metadata for the block, or null if not found. 99 */ 100 public static function get_metadata( $file_or_folder ) { 101 $path = self::find_collection_path( $file_or_folder ); 102 if ( ! $path ) { 103 return null; 104 } 105 106 $collection = &self::$collections[ $path ]; 107 108 if ( null === $collection['metadata'] ) { 109 // Load the manifest file if not already loaded 110 $collection['metadata'] = require $collection['manifest']; 111 } 112 113 // Use the identifier callback to get the block name, or the default callback if not set. 114 $identifier_callback = self::$collections[ $path ]['identifier_callback']; 115 if ( is_null( $identifier_callback ) ) { 116 $block_name = self::default_identifier_callback( $file_or_folder ); 117 } else { 118 $block_name = call_user_func( $identifier_callback, $file_or_folder ); 119 } 120 121 return isset( $collection['metadata'][ $block_name ] ) ? $collection['metadata'][ $block_name ] : null; 122 } 123 124 /** 125 * Finds the collection path for a given file or folder. 126 * 127 * @since 6.X.0 128 * 129 * @param string $file_or_folder The path to the file or folder. 130 * @return string|null The collection path if found, or null if not found. 131 */ 132 private static function find_collection_path( $file_or_folder ) { 133 if ( empty( $file_or_folder ) ) { 134 return null; 135 } 136 137 // Check the last matched collection first, since block registration usually happens in batches per plugin or theme. 138 $path = wp_normalize_path( rtrim( $file_or_folder, '/' ) ); 139 if ( self::$last_matched_collection && str_starts_with( $path, self::$last_matched_collection ) ) { 140 return self::$last_matched_collection; 141 } 142 143 $collection_paths = array_keys( self::$collections ); 144 foreach ( $collection_paths as $collection_path ) { 145 if ( str_starts_with( $path, $collection_path ) ) { 146 self::$last_matched_collection = $collection_path; 147 return $collection_path; 148 } 149 } 150 return null; 151 } 152 153 /** 154 * Checks if metadata exists for a given block name in a specific collection. 155 * 156 * @since 6.X.0 157 * 158 * @param string $file_or_folder The path to the file or folder containing the block metadata. 159 * @return bool True if metadata exists for the block, false otherwise. 160 */ 161 public static function has_metadata( $file_or_folder ) { 162 return null !== self::get_metadata( $file_or_folder ); 163 } 164 165 /** 166 * Default callback function to determine the block identifier from a given path. 167 * 168 * This function is used when no custom identifier callback is provided during 169 * collection registration. It extracts the block identifier from the path: 170 * - For 'block.json' files, it uses the parent directory name. 171 * - For directories, it uses the directory name itself. 172 * 173 * For example: 174 * - Path: '/wp-content/plugins/my-plugin/blocks/example/block.json' 175 * Identifier: 'example' 176 * - Path: '/wp-content/plugins/my-plugin/blocks/another-block' 177 * Identifier: 'another-block' 178 * 179 * This default behavior matches the standard WordPress block structure. 180 * Custom callbacks can be provided for non-standard structures. 181 * 182 * @since 6.X.0 183 * 184 * @param string $path The file or folder path to determine the block identifier from. 185 * @return string The block identifier. 186 */ 187 public static function default_identifier_callback( $path ) { 188 if ( substr( $path, -10 ) === 'block.json' ) { 189 // If it's block.json, use the parent directory name. 190 return basename( dirname( $path ) ); 191 } else { 192 // Otherwise, assume it's a directory and use its name. 193 return basename( $path ); 194 } 195 } 196 197 /** 198 * Private constructor to prevent instantiation. 199 */ 200 private function __construct() { 201 // Prevent instantiation 202 } 203 } -
src/wp-settings.php
diff --git a/src/wp-settings.php b/src/wp-settings.php index 4643892ada..e3bd5ca58d 100644
a b require ABSPATH . WPINC . '/class-wp-block-styles-registry.php'; 354 354 require ABSPATH . WPINC . '/class-wp-block-type-registry.php'; 355 355 require ABSPATH . WPINC . '/class-wp-block.php'; 356 356 require ABSPATH . WPINC . '/class-wp-block-list.php'; 357 require ABSPATH . WPINC . '/class-wp-block-metadata-registry.php'; 357 358 require ABSPATH . WPINC . '/class-wp-block-parser-block.php'; 358 359 require ABSPATH . WPINC . '/class-wp-block-parser-frame.php'; 359 360 require ABSPATH . WPINC . '/class-wp-block-parser.php'; -
new file tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php
diff --git a/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php b/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php new file mode 100644 index 0000000000..455eb5de04
- + 1 <?php 2 3 /** 4 * Tests for WP_Block_Metadata_Registry class. 5 * 6 * @group blocks 7 */ 8 class Tests_Blocks_WpBlockMetadataRegistry extends WP_UnitTestCase { 9 10 private $temp_manifest_file; 11 12 public function set_up() { 13 parent::set_up(); 14 $this->temp_manifest_file = wp_tempnam( 'block-metadata-manifest' ); 15 } 16 17 public function tear_down() { 18 unlink( $this->temp_manifest_file ); 19 parent::tear_down(); 20 } 21 22 public function test_register_collection_and_get_metadata() { 23 $path = WP_PLUGIN_DIR . '/test/path'; 24 $manifest_data = array( 25 'test-block' => array( 26 'name' => 'test-block', 27 'title' => 'Test Block', 28 ), 29 ); 30 31 file_put_contents( $this->temp_manifest_file, '<?php return ' . var_export( $manifest_data, true ) . ';' ); 32 33 WP_Block_Metadata_Registry::register_collection( $path, $this->temp_manifest_file ); 34 35 $retrieved_metadata = WP_Block_Metadata_Registry::get_metadata( $path . '/test-block' ); 36 $this->assertEquals( $manifest_data['test-block'], $retrieved_metadata ); 37 } 38 39 public function test_get_nonexistent_metadata() { 40 $path = WP_PLUGIN_DIR . '/nonexistent/path'; 41 $retrieved_metadata = WP_Block_Metadata_Registry::get_metadata( $path . '/nonexistent-block' ); 42 $this->assertNull( $retrieved_metadata ); 43 } 44 45 public function test_has_metadata() { 46 $path = WP_PLUGIN_DIR . '/another/test/path'; 47 $manifest_data = array( 48 'existing-block' => array( 49 'name' => 'existing-block', 50 'title' => 'Existing Block', 51 ), 52 ); 53 54 file_put_contents( $this->temp_manifest_file, '<?php return ' . var_export( $manifest_data, true ) . ';' ); 55 56 WP_Block_Metadata_Registry::register_collection( $path, $this->temp_manifest_file ); 57 58 $this->assertTrue( WP_Block_Metadata_Registry::has_metadata( $path . '/existing-block' ) ); 59 $this->assertFalse( WP_Block_Metadata_Registry::has_metadata( $path . '/non-existing-block' ) ); 60 } 61 62 public function test_register_collection_with_core_path() { 63 $core_path = ABSPATH . WPINC . '/blocks'; 64 $result = WP_Block_Metadata_Registry::register_collection( $core_path, $this->temp_manifest_file ); 65 $this->assertTrue( $result, 'Core path should be registered successfully' ); 66 } 67 68 public function test_register_collection_with_valid_plugin_path() { 69 $plugin_path = WP_PLUGIN_DIR . '/my-plugin/blocks'; 70 $result = WP_Block_Metadata_Registry::register_collection( $plugin_path, $this->temp_manifest_file ); 71 $this->assertTrue( $result, 'Valid plugin path should be registered successfully' ); 72 } 73 74 public function test_register_collection_with_invalid_plugin_path() { 75 $invalid_plugin_path = WP_PLUGIN_DIR; 76 $result = WP_Block_Metadata_Registry::register_collection( $invalid_plugin_path, $this->temp_manifest_file ); 77 $this->assertFalse( $result, 'Invalid plugin path should not be registered' ); 78 } 79 80 public function test_register_collection_with_non_existent_path() { 81 $non_existent_path = '/path/that/does/not/exist'; 82 $result = WP_Block_Metadata_Registry::register_collection( $non_existent_path, $this->temp_manifest_file ); 83 $this->assertFalse( $result, 'Non-existent path should not be registered' ); 84 } 85 }