Make WordPress Core

Ticket #62002: add-metadata-registry-v4.diff

File add-metadata-registry-v4.diff, 15.0 KB (added by mreishus, 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() { 
    375375        return $i18n_block_schema;
    376376}
    377377
     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 */
     390function wp_register_block_metadata_collection( $path, $manifest ) {
     391        WP_Block_Metadata_Registry::register_collection( $path, $manifest );
     392}
     393
    378394/**
    379395 * Registers a block type from the metadata stored in the `block.json` file.
    380396 *
    function register_block_type_from_metadata( $file_or_folder, $args = array() ) { 
    402418         * instead of reading a JSON file per-block, and then decoding from JSON to PHP.
    403419         * Using a static variable ensures that the metadata is only read once per request.
    404420         */
    405         static $core_blocks_meta;
    406         if ( ! $core_blocks_meta ) {
    407                 $core_blocks_meta = require ABSPATH . WPINC . '/blocks/blocks-json.php';
    408         }
    409421
    410422        $metadata_file = ( ! str_ends_with( $file_or_folder, 'block.json' ) ) ?
    411423                trailingslashit( $file_or_folder ) . 'block.json' :
    412424                $file_or_folder;
    413425
    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 );
    417429        if ( ! $metadata_file_exists && empty( $args['name'] ) ) {
    418430                return false;
    419431        }
    420432
    421433        // Try to get metadata from the static cache for core blocks.
    422434        $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;
    428437        }
    429438
    430439        // 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() { 
    155155        }
    156156}
    157157add_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 */
     168function wp_register_core_block_metadata_collection() {
     169        wp_register_block_metadata_collection(
     170                BLOCKS_PATH,
     171                BLOCKS_PATH . 'blocks-json.php'
     172        );
     173}
     174add_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 */
     20class 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'; 
    354354require ABSPATH . WPINC . '/class-wp-block-type-registry.php';
    355355require ABSPATH . WPINC . '/class-wp-block.php';
    356356require ABSPATH . WPINC . '/class-wp-block-list.php';
     357require ABSPATH . WPINC . '/class-wp-block-metadata-registry.php';
    357358require ABSPATH . WPINC . '/class-wp-block-parser-block.php';
    358359require ABSPATH . WPINC . '/class-wp-block-parser-frame.php';
    359360require 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 */
     8class 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}