From fbcb0843059cb1f80edb8826d8ae82e87f1a98b3 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 30 Sep 2024 17:08:26 +0000 Subject: [PATCH] Editor: Allow registering PHP manifest file for block metadata collections for enhanced performance. Typically, when registering a new block type, its metadata is read from the provided `block.json` file. The more block types are registered on a site, the more costly becomes this process, as it involves filesystem reads and parsing JSON. WordPress Core's built-in blocks have in the past worked around that by having a auto-generated PHP manifest file that includes the already parsed JSON data for all blocks. This changeset effectively allows plugins to do the same, by introducing a new API function `wp_register_block_metadata_collection()`. The WordPress Core block manifest is now handled using this API as well, rather than custom logic baked into `register_block_type_from_metadata()`. The `wp_register_block_metadata_collection()` function requires two parameters: * `$path`: The base path in which block files for the collection reside. * `$manifest`: The path to the manifest file for the collection. Every `block.json` file that is supposed to be part of the collection must reside within the provided `$path`, within its own block-specific directory matching the block name (without the block namespace). For example, for a collection `$path` of `/wp-content/plugins/test-plugin` and a block `test-plugin/testimonial`, the block file could be `/wp-content/plugins/test-plugins/blocks/testimonial/block.json`. It is recommended that plugins use the new API function for enhanced performance, especially if they register several block types. However, the use of the function is entirely optional. Not using it will not result in any difference in user-facing behavior. Props mreishus, flixos90, gziolo, spacedmonkey, azaozz, mukesh27. Fixes #62002. Built from https://develop.svn.wordpress.org/trunk@59132 git-svn-id: http://core.svn.wordpress.org/trunk@58528 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/blocks.php | 43 +-- wp-includes/blocks/index.php | 17 ++ .../class-wp-block-metadata-registry.php | 273 ++++++++++++++++++ wp-includes/version.php | 2 +- wp-settings.php | 1 + 5 files changed, 315 insertions(+), 21 deletions(-) create mode 100644 wp-includes/class-wp-block-metadata-registry.php diff --git a/wp-includes/blocks.php b/wp-includes/blocks.php index ca84130995..34f1d921dd 100644 --- a/wp-includes/blocks.php +++ b/wp-includes/blocks.php @@ -375,6 +375,22 @@ function get_block_metadata_i18n_schema() { return $i18n_block_schema; } +/** + * Registers a block metadata collection. + * + * This function allows core and third-party plugins to register their block metadata + * collections in a centralized location. Registering collections can improve performance + * by avoiding multiple reads from the filesystem and parsing JSON. + * + * @since 6.7.0 + * + * @param string $path The base path in which block files for the collection reside. + * @param string $manifest The path to the manifest file for the collection. + */ +function wp_register_block_metadata_collection( $path, $manifest ) { + WP_Block_Metadata_Registry::register_collection( $path, $manifest ); +} + /** * Registers a block type from the metadata stored in the `block.json` file. * @@ -402,34 +418,21 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { * instead of reading a JSON file per-block, and then decoding from JSON to PHP. * Using a static variable ensures that the metadata is only read once per request. */ - static $core_blocks_meta; - if ( ! $core_blocks_meta ) { - $core_blocks_meta = require ABSPATH . WPINC . '/blocks/blocks-json.php'; - } $metadata_file = ( ! str_ends_with( $file_or_folder, 'block.json' ) ) ? trailingslashit( $file_or_folder ) . 'block.json' : $file_or_folder; - $is_core_block = str_starts_with( $file_or_folder, ABSPATH . WPINC ); - // If the block is not a core block, the metadata file must exist. + $is_core_block = str_starts_with( $file_or_folder, ABSPATH . WPINC ); $metadata_file_exists = $is_core_block || file_exists( $metadata_file ); - if ( ! $metadata_file_exists && empty( $args['name'] ) ) { - return false; - } + $registry_metadata = WP_Block_Metadata_Registry::get_metadata( $file_or_folder ); - // Try to get metadata from the static cache for core blocks. - $metadata = array(); - if ( $is_core_block ) { - $core_block_name = str_replace( ABSPATH . WPINC . '/blocks/', '', $file_or_folder ); - if ( ! empty( $core_blocks_meta[ $core_block_name ] ) ) { - $metadata = $core_blocks_meta[ $core_block_name ]; - } - } - - // If metadata is not found in the static cache, read it from the file. - if ( $metadata_file_exists && empty( $metadata ) ) { + if ( $registry_metadata ) { + $metadata = $registry_metadata; + } elseif ( $metadata_file_exists ) { $metadata = wp_json_file_decode( $metadata_file, array( 'associative' => true ) ); + } else { + $metadata = array(); } if ( ! is_array( $metadata ) || ( empty( $metadata['name'] ) && empty( $args['name'] ) ) ) { diff --git a/wp-includes/blocks/index.php b/wp-includes/blocks/index.php index 360d14d62d..0e4412ba6d 100644 --- a/wp-includes/blocks/index.php +++ b/wp-includes/blocks/index.php @@ -160,3 +160,20 @@ function register_core_block_types_from_metadata() { } } add_action( 'init', 'register_core_block_types_from_metadata' ); + +/** + * Registers the core block metadata collection. + * + * This function is hooked into the 'init' action with a priority of 9, + * ensuring that the core block metadata is registered before the regular + * block initialization that happens at priority 10. + * + * @since 6.7.0 + */ +function wp_register_core_block_metadata_collection() { + wp_register_block_metadata_collection( + BLOCKS_PATH, + BLOCKS_PATH . 'blocks-json.php' + ); +} +add_action( 'init', 'wp_register_core_block_metadata_collection', 9 ); diff --git a/wp-includes/class-wp-block-metadata-registry.php b/wp-includes/class-wp-block-metadata-registry.php new file mode 100644 index 0000000000..17bbf320d5 --- /dev/null +++ b/wp-includes/class-wp-block-metadata-registry.php @@ -0,0 +1,273 @@ +> + */ + private static $collections = array(); + + /** + * Caches the last matched collection path for performance optimization. + * + * @since 6.7.0 + * @var string|null + */ + private static $last_matched_collection = null; + + /** + * Stores the WordPress 'wp-includes' directory path. + * + * @since 6.7.0 + * @var string|null + */ + private static $wpinc_dir = null; + + /** + * Stores the normalized WordPress plugin directory path. + * + * @since 6.7.0 + * @var string|null + */ + private static $plugin_dir = null; + + /** + * Registers a block metadata collection. + * + * This method allows registering a collection of block metadata from a single + * manifest file, improving performance for large sets of blocks. + * + * The manifest file should be a PHP file that returns an associative array, where + * the keys are the block identifiers (without their namespace) and the values are + * the corresponding block metadata arrays. The block identifiers must match the + * parent directory name for the respective `block.json` file. + * + * Example manifest file structure: + * ``` + * return array( + * 'example-block' => array( + * 'title' => 'Example Block', + * 'category' => 'widgets', + * 'icon' => 'smiley', + * // ... other block metadata + * ), + * 'another-block' => array( + * 'title' => 'Another Block', + * 'category' => 'formatting', + * 'icon' => 'star-filled', + * // ... other block metadata + * ), + * // ... more block metadata entries + * ); + * ``` + * + * @since 6.7.0 + * + * @param string $path The absolute base path for the collection ( e.g., WP_PLUGIN_DIR . '/my-plugin/blocks/' ). + * @param string $manifest The absolute path to the manifest file containing the metadata collection. + * @return bool True if the collection was registered successfully, false otherwise. + */ + public static function register_collection( $path, $manifest ) { + $path = wp_normalize_path( rtrim( $path, '/' ) ); + + $wpinc_dir = self::get_wpinc_dir(); + $plugin_dir = self::get_plugin_dir(); + + // Check if the path is valid: + if ( str_starts_with( $path, $plugin_dir ) ) { + // For plugins, ensure the path is within a specific plugin directory and not the base plugin directory. + $relative_path = substr( $path, strlen( $plugin_dir ) + 1 ); + $plugin_name = strtok( $relative_path, '/' ); + + if ( empty( $plugin_name ) || $plugin_name === $relative_path ) { + _doing_it_wrong( + __METHOD__, + __( 'Block metadata collections can only be registered for a specific plugin. The provided path is neither a core path nor a valid plugin path.' ), + '6.7.0' + ); + return false; + } + } elseif ( ! str_starts_with( $path, $wpinc_dir ) ) { + // If it's neither a plugin directory path nor within 'wp-includes', the path is invalid. + _doing_it_wrong( + __METHOD__, + __( 'Block metadata collections can only be registered for a specific plugin. The provided path is neither a core path nor a valid plugin path.' ), + '6.7.0' + ); + return false; + } + + if ( ! file_exists( $manifest ) ) { + _doing_it_wrong( + __METHOD__, + __( 'The specified manifest file does not exist.' ), + '6.7.0' + ); + return false; + } + + self::$collections[ $path ] = array( + 'manifest' => $manifest, + 'metadata' => null, + ); + + return true; + } + + /** + * Retrieves block metadata for a given block within a specific collection. + * + * This method uses the registered collections to efficiently lookup + * block metadata without reading individual `block.json` files. + * + * @since 6.7.0 + * + * @param string $file_or_folder The path to the file or folder containing the block. + * @return array|null The block metadata for the block, or null if not found. + */ + public static function get_metadata( $file_or_folder ) { + $path = self::find_collection_path( $file_or_folder ); + if ( ! $path ) { + return null; + } + + $collection = &self::$collections[ $path ]; + + if ( null === $collection['metadata'] ) { + // Load the manifest file if not already loaded + $collection['metadata'] = require $collection['manifest']; + } + + // Get the block name from the path. + $block_name = self::default_identifier_callback( $file_or_folder ); + + return isset( $collection['metadata'][ $block_name ] ) ? $collection['metadata'][ $block_name ] : null; + } + + /** + * Finds the collection path for a given file or folder. + * + * @since 6.7.0 + * + * @param string $file_or_folder The path to the file or folder. + * @return string|null The collection path if found, or null if not found. + */ + private static function find_collection_path( $file_or_folder ) { + if ( empty( $file_or_folder ) ) { + return null; + } + + // Check the last matched collection first, since block registration usually happens in batches per plugin or theme. + $path = wp_normalize_path( rtrim( $file_or_folder, '/' ) ); + if ( self::$last_matched_collection && str_starts_with( $path, self::$last_matched_collection ) ) { + return self::$last_matched_collection; + } + + $collection_paths = array_keys( self::$collections ); + foreach ( $collection_paths as $collection_path ) { + if ( str_starts_with( $path, $collection_path ) ) { + self::$last_matched_collection = $collection_path; + return $collection_path; + } + } + return null; + } + + /** + * Checks if metadata exists for a given block name in a specific collection. + * + * @since 6.7.0 + * + * @param string $file_or_folder The path to the file or folder containing the block metadata. + * @return bool True if metadata exists for the block, false otherwise. + */ + public static function has_metadata( $file_or_folder ) { + return null !== self::get_metadata( $file_or_folder ); + } + + /** + * Default identifier function to determine the block identifier from a given path. + * + * This function extracts the block identifier from the path: + * - For 'block.json' files, it uses the parent directory name. + * - For directories, it uses the directory name itself. + * - For empty paths, it returns an empty string. + * + * For example: + * - Path: '/wp-content/plugins/my-plugin/blocks/example/block.json' + * Identifier: 'example' + * - Path: '/wp-content/plugins/my-plugin/blocks/another-block' + * Identifier: 'another-block' + * + * This default behavior matches the standard WordPress block structure. + * + * @since 6.7.0 + * + * @param string $path The file or folder path to determine the block identifier from. + * @return string The block identifier, or an empty string if the path is empty. + */ + private static function default_identifier_callback( $path ) { + // Ensure $path is not empty to prevent unexpected behavior. + if ( empty( $path ) ) { + return ''; + } + + if ( str_ends_with( $path, 'block.json' ) ) { + // Return the parent directory name if it's a block.json file. + return basename( dirname( $path ) ); + } + + // Otherwise, assume it's a directory and return its name. + return basename( $path ); + } + + /** + * Gets the WordPress 'wp-includes' directory path. + * + * @since 6.7.0 + * + * @return string The WordPress 'wp-includes' directory path. + */ + private static function get_wpinc_dir() { + if ( ! isset( self::$wpinc_dir ) ) { + self::$wpinc_dir = wp_normalize_path( ABSPATH . WPINC ); + } + return self::$wpinc_dir; + } + + /** + * Gets the normalized WordPress plugin directory path. + * + * @since 6.7.0 + * + * @return string The normalized WordPress plugin directory path. + */ + private static function get_plugin_dir() { + if ( ! isset( self::$plugin_dir ) ) { + self::$plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ); + } + return self::$plugin_dir; + } +} diff --git a/wp-includes/version.php b/wp-includes/version.php index 8064505eda..0daba56923 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.7-alpha-59131'; +$wp_version = '6.7-alpha-59132'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. diff --git a/wp-settings.php b/wp-settings.php index b4cebee435..62ba4d5dee 100644 --- a/wp-settings.php +++ b/wp-settings.php @@ -355,6 +355,7 @@ require ABSPATH . WPINC . '/class-wp-block-styles-registry.php'; require ABSPATH . WPINC . '/class-wp-block-type-registry.php'; require ABSPATH . WPINC . '/class-wp-block.php'; require ABSPATH . WPINC . '/class-wp-block-list.php'; +require ABSPATH . WPINC . '/class-wp-block-metadata-registry.php'; require ABSPATH . WPINC . '/class-wp-block-parser-block.php'; require ABSPATH . WPINC . '/class-wp-block-parser-frame.php'; require ABSPATH . WPINC . '/class-wp-block-parser.php';