-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extend Block Metadata PHP Cache to Third-Party Blocks #7303
Changes from 54 commits
5f5fbd2
8a28294
8ca134d
270d784
9376059
d503724
c5b81f5
a914dee
a25c643
bb6fec3
c7cb99a
14b1c56
f75c4fd
12c01b8
ae81aa6
9779e78
15c51a6
2c3630b
1fa5877
a4c1455
26c6b0c
191c553
43915a8
abbf5d5
8d7badc
0ca6c93
70431b2
504e09c
db5dee2
61acb57
4cd8fcc
3b13d45
396e6c9
78a46fb
d948dde
15353f4
a3b73f3
3a5ad69
3b70192
166ed80
63772f1
b069b60
2e44638
779a2e8
a9d6eb9
a257e6f
3e4d5b6
3a55080
6584762
f2e31d7
2d7533b
498bbba
c23cf0a
7fd96b4
9aa134d
a323758
6467b31
31809da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
<?php | ||
/** | ||
* Block Metadata Registry | ||
* | ||
* @package WordPress | ||
* @subpackage Blocks | ||
* @since 6.7.0 | ||
*/ | ||
|
||
/** | ||
* Class used for managing block metadata collections. | ||
* | ||
* The WP_Block_Metadata_Registry allows plugins to register metadata for large | ||
* collections of blocks (e.g., 50-100+) using a single PHP file. This approach | ||
* reduces the need to read and decode multiple `block.json` files, enhancing | ||
* performance through opcode caching. | ||
* | ||
* @since 6.7.0 | ||
*/ | ||
class WP_Block_Metadata_Registry { | ||
|
||
/** | ||
* Container for storing block metadata collections. | ||
* | ||
* Each entry maps a base path to its corresponding metadata and callback. | ||
* | ||
* @since 6.7.0 | ||
* @var array<string, array<string, mixed>> | ||
*/ | ||
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 include path. | ||
* | ||
* @since 6.7.0 | ||
* @var string | ||
*/ | ||
private static $wpinc_path = ''; | ||
|
||
/** | ||
* Stores the normalized WordPress plugin directory path. | ||
* | ||
* @since 6.7.0 | ||
* @var string | ||
*/ | ||
private static $plugin_dir = ''; | ||
|
||
/** | ||
* 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The documentation included is very helpful. I think I fully understand now how it all would work. If there were any assets (CSS, JS, render.php, etc), they probably should be located in these subfolders. |
||
* 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_path = self::get_wpinc_dir(); | ||
$plugin_dir = self::get_plugin_dir(); | ||
|
||
// Check if the path is valid: | ||
if ( str_starts_with( $path, $wpinc_path ) ) { | ||
// Core path is valid. | ||
} elseif ( str_starts_with( $path, $plugin_dir ) ) { | ||
felixarntz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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; | ||
} | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The block registering API support registering blocks in theme directory. Both theme and child theme. Why is this not supported here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. I originally made this to help out the plugins that were registering large collection of blocks, but didn't even consider that themes could be doing the same thing. I just now allowed the theme directory here: e93a3d1, although I don't know of any such themes to check if they use the same directory convention.. ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mreishus Two thoughts on this:
My preference at this point would be to remove this again and focus on it separately without being in a rush. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I had set up that code but somehow missed it in my commit.. that does point out to me being in a rush and making some silly mistakes. I think pushing this back is a good idea. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I reverted the themes changes for now. Can re-examine with more time later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree with @felixarntz here. I think we need to support theme resignation here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on what? I gave two good reasons why this doesn't have to be included. Please provide a rationale why you think this needs to be included. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I opened https://core.trac.wordpress.org/ticket/62140 for us to discuss this further, to not unnecessarily block this PR.
felixarntz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_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; | ||
} | ||
|
||
mreishus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
mreishus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$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 include path. | ||
* | ||
* @since 6.7.0 | ||
* | ||
* @return string The WordPress include path. | ||
*/ | ||
private static function get_wpinc_dir() { | ||
if ( empty( self::$wpinc_path ) ) { | ||
felixarntz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self::$wpinc_path = wp_normalize_path( ABSPATH . WPINC ); | ||
} | ||
return self::$wpinc_path; | ||
} | ||
|
||
/** | ||
* 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 ( empty( self::$plugin_dir ) ) { | ||
felixarntz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self::$plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ); | ||
} | ||
return self::$plugin_dir; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So my hope for the future is the following to be possible in Gutenberg:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right! I think technically that already should be possible once this PR lands? 🤔