[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-block-metadata-registry.php (source)

   1  <?php
   2  /**
   3   * Block Metadata Registry
   4   *
   5   * @package WordPress
   6   * @subpackage Blocks
   7   * @since 6.7.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.7.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.7.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.7.0
  36       * @var string|null
  37       */
  38      private static $last_matched_collection = null;
  39  
  40      /**
  41       * Stores the default allowed collection root paths.
  42       *
  43       * @since 6.7.2
  44       * @var string[]|null
  45       */
  46      private static $default_collection_roots = null;
  47  
  48      /**
  49       * Registers a block metadata collection.
  50       *
  51       * This method allows registering a collection of block metadata from a single
  52       * manifest file, improving performance for large sets of blocks.
  53       *
  54       * The manifest file should be a PHP file that returns an associative array, where
  55       * the keys are the block identifiers (without their namespace) and the values are
  56       * the corresponding block metadata arrays. The block identifiers must match the
  57       * parent directory name for the respective `block.json` file.
  58       *
  59       * Example manifest file structure:
  60       * ```
  61       * return array(
  62       *     'example-block' => array(
  63       *         'title' => 'Example Block',
  64       *         'category' => 'widgets',
  65       *         'icon' => 'smiley',
  66       *         // ... other block metadata
  67       *     ),
  68       *     'another-block' => array(
  69       *         'title' => 'Another Block',
  70       *         'category' => 'formatting',
  71       *         'icon' => 'star-filled',
  72       *         // ... other block metadata
  73       *     ),
  74       *     // ... more block metadata entries
  75       * );
  76       * ```
  77       *
  78       * @since 6.7.0
  79       *
  80       * @param string $path     The absolute base path for the collection ( e.g., WP_PLUGIN_DIR . '/my-plugin/blocks/' ).
  81       * @param string $manifest The absolute path to the manifest file containing the metadata collection.
  82       * @return bool True if the collection was registered successfully, false otherwise.
  83       */
  84  	public static function register_collection( $path, $manifest ) {
  85          $path = wp_normalize_path( rtrim( $path, '/' ) );
  86  
  87          $collection_roots = self::get_default_collection_roots();
  88  
  89          /**
  90           * Filters the root directory paths for block metadata collections.
  91           *
  92           * Any block metadata collection that is registered must not use any of these paths, or any parent directory
  93           * path of them. Most commonly, block metadata collections should reside within one of these paths, though in
  94           * some scenarios they may also reside in entirely different directories (e.g. in case of symlinked plugins).
  95           *
  96           * Example:
  97           * * It is allowed to register a collection with path `WP_PLUGIN_DIR . '/my-plugin'`.
  98           * * It is not allowed to register a collection with path `WP_PLUGIN_DIR`.
  99           * * It is not allowed to register a collection with path `dirname( WP_PLUGIN_DIR )`.
 100           *
 101           * The default list encompasses the `wp-includes` directory, as well as the root directories for plugins,
 102           * must-use plugins, and themes. This filter can be used to expand the list, e.g. to custom directories that
 103           * contain symlinked plugins, so that these root directories cannot be used themselves for a block metadata
 104           * collection either.
 105           *
 106           * @since 6.7.2
 107           *
 108           * @param string[] $collection_roots List of allowed metadata collection root paths.
 109           */
 110          $collection_roots = apply_filters( 'wp_allowed_block_metadata_collection_roots', $collection_roots );
 111  
 112          $collection_roots = array_unique(
 113              array_map(
 114                  static function ( $allowed_root ) {
 115                      return rtrim( $allowed_root, '/' );
 116                  },
 117                  $collection_roots
 118              )
 119          );
 120  
 121          // Check if the path is valid:
 122          if ( ! self::is_valid_collection_path( $path, $collection_roots ) ) {
 123              _doing_it_wrong(
 124                  __METHOD__,
 125                  sprintf(
 126                      /* translators: %s: list of allowed collection roots */
 127                      __( 'Block metadata collections cannot be registered as one of the following directories or their parent directories: %s' ),
 128                      esc_html( implode( wp_get_list_item_separator(), $collection_roots ) )
 129                  ),
 130                  '6.7.2'
 131              );
 132              return false;
 133          }
 134  
 135          if ( ! file_exists( $manifest ) ) {
 136              _doing_it_wrong(
 137                  __METHOD__,
 138                  __( 'The specified manifest file does not exist.' ),
 139                  '6.7.0'
 140              );
 141              return false;
 142          }
 143  
 144          self::$collections[ $path ] = array(
 145              'manifest' => $manifest,
 146              'metadata' => null,
 147          );
 148  
 149          return true;
 150      }
 151  
 152      /**
 153       * Retrieves block metadata for a given block within a specific collection.
 154       *
 155       * This method uses the registered collections to efficiently lookup
 156       * block metadata without reading individual `block.json` files.
 157       *
 158       * @since 6.7.0
 159       *
 160       * @param string $file_or_folder The path to the file or folder containing the block.
 161       * @return array|null The block metadata for the block, or null if not found.
 162       */
 163  	public static function get_metadata( $file_or_folder ) {
 164          $path = self::find_collection_path( $file_or_folder );
 165          if ( ! $path ) {
 166              return null;
 167          }
 168  
 169          $collection = &self::$collections[ $path ];
 170  
 171          if ( null === $collection['metadata'] ) {
 172              // Load the manifest file if not already loaded
 173              $collection['metadata'] = require $collection['manifest'];
 174          }
 175  
 176          // Get the block name from the path.
 177          $block_name = self::default_identifier_callback( $file_or_folder );
 178  
 179          return isset( $collection['metadata'][ $block_name ] ) ? $collection['metadata'][ $block_name ] : null;
 180      }
 181  
 182      /**
 183       * Finds the collection path for a given file or folder.
 184       *
 185       * @since 6.7.0
 186       *
 187       * @param string $file_or_folder The path to the file or folder.
 188       * @return string|null The collection path if found, or null if not found.
 189       */
 190  	private static function find_collection_path( $file_or_folder ) {
 191          if ( empty( $file_or_folder ) ) {
 192              return null;
 193          }
 194  
 195          // Check the last matched collection first, since block registration usually happens in batches per plugin or theme.
 196          $path = wp_normalize_path( rtrim( $file_or_folder, '/' ) );
 197          if ( self::$last_matched_collection && str_starts_with( $path, self::$last_matched_collection ) ) {
 198              return self::$last_matched_collection;
 199          }
 200  
 201          $collection_paths = array_keys( self::$collections );
 202          foreach ( $collection_paths as $collection_path ) {
 203              if ( str_starts_with( $path, $collection_path ) ) {
 204                  self::$last_matched_collection = $collection_path;
 205                  return $collection_path;
 206              }
 207          }
 208          return null;
 209      }
 210  
 211      /**
 212       * Checks if metadata exists for a given block name in a specific collection.
 213       *
 214       * @since 6.7.0
 215       *
 216       * @param string $file_or_folder The path to the file or folder containing the block metadata.
 217       * @return bool True if metadata exists for the block, false otherwise.
 218       */
 219  	public static function has_metadata( $file_or_folder ) {
 220          return null !== self::get_metadata( $file_or_folder );
 221      }
 222  
 223      /**
 224       * Default identifier function to determine the block identifier from a given path.
 225       *
 226       * This function extracts the block identifier from the path:
 227       * - For 'block.json' files, it uses the parent directory name.
 228       * - For directories, it uses the directory name itself.
 229       * - For empty paths, it returns an empty string.
 230       *
 231       * For example:
 232       * - Path: '/wp-content/plugins/my-plugin/blocks/example/block.json'
 233       *   Identifier: 'example'
 234       * - Path: '/wp-content/plugins/my-plugin/blocks/another-block'
 235       *   Identifier: 'another-block'
 236       *
 237       * This default behavior matches the standard WordPress block structure.
 238       *
 239       * @since 6.7.0
 240       *
 241       * @param string $path The file or folder path to determine the block identifier from.
 242       * @return string The block identifier, or an empty string if the path is empty.
 243       */
 244  	private static function default_identifier_callback( $path ) {
 245          // Ensure $path is not empty to prevent unexpected behavior.
 246          if ( empty( $path ) ) {
 247              return '';
 248          }
 249  
 250          if ( str_ends_with( $path, 'block.json' ) ) {
 251              // Return the parent directory name if it's a block.json file.
 252              return basename( dirname( $path ) );
 253          }
 254  
 255          // Otherwise, assume it's a directory and return its name.
 256          return basename( $path );
 257      }
 258  
 259      /**
 260       * Checks whether the given block metadata collection path is valid against the list of collection roots.
 261       *
 262       * @since 6.7.2
 263       *
 264       * @param string   $path             Block metadata collection path, without trailing slash.
 265       * @param string[] $collection_roots List of collection root paths, without trailing slashes.
 266       * @return bool True if the path is allowed, false otherwise.
 267       */
 268  	private static function is_valid_collection_path( $path, $collection_roots ) {
 269          foreach ( $collection_roots as $allowed_root ) {
 270              // If the path matches any root exactly, it is invalid.
 271              if ( $allowed_root === $path ) {
 272                  return false;
 273              }
 274  
 275              // If the path is a parent path of any of the roots, it is invalid.
 276              if ( str_starts_with( $allowed_root, $path ) ) {
 277                  return false;
 278              }
 279          }
 280  
 281          return true;
 282      }
 283  
 284      /**
 285       * Gets the default collection root directory paths.
 286       *
 287       * @since 6.7.2
 288       *
 289       * @return string[] List of directory paths within which metadata collections are allowed.
 290       */
 291  	private static function get_default_collection_roots() {
 292          if ( isset( self::$default_collection_roots ) ) {
 293              return self::$default_collection_roots;
 294          }
 295  
 296          $collection_roots = array(
 297              wp_normalize_path( ABSPATH . WPINC ),
 298              wp_normalize_path( WP_CONTENT_DIR ),
 299              wp_normalize_path( WPMU_PLUGIN_DIR ),
 300              wp_normalize_path( WP_PLUGIN_DIR ),
 301          );
 302  
 303          $theme_roots = get_theme_roots();
 304          if ( ! is_array( $theme_roots ) ) {
 305              $theme_roots = array( $theme_roots );
 306          }
 307          foreach ( $theme_roots as $theme_root ) {
 308              $collection_roots[] = trailingslashit( wp_normalize_path( WP_CONTENT_DIR ) ) . ltrim( wp_normalize_path( $theme_root ), '/' );
 309          }
 310  
 311          self::$default_collection_roots = array_unique( $collection_roots );
 312          return self::$default_collection_roots;
 313      }
 314  }


Generated : Fri Feb 21 08:20:01 2025 Cross-referenced by PHPXref