Skip to content
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

Closed
wants to merge 58 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
5f5fbd2
add WP_Block_Metadata_Registry, allow blocks to bypass block.json par…
mreishus Sep 5, 2024
8a28294
update
mreishus Sep 5, 2024
8ca134d
add metadata_source
mreishus Sep 5, 2024
270d784
formatting fixes
mreishus Sep 6, 2024
9376059
add trailing comma
mreishus Sep 6, 2024
d503724
horizontal align
mreishus Sep 6, 2024
c5b81f5
remove namespace
mreishus Sep 13, 2024
a914dee
remove third parameter
mreishus Sep 13, 2024
a25c643
make static
mreishus Sep 13, 2024
bb6fec3
allow metadata source to be passed to register_block_type
mreishus Sep 13, 2024
c7cb99a
fix alignment
mreishus Sep 13, 2024
14b1c56
undo everything
mreishus Sep 23, 2024
f75c4fd
new implementation
mreishus Sep 23, 2024
12c01b8
add tests
mreishus Sep 23, 2024
ae81aa6
align equals
mreishus Sep 23, 2024
9779e78
fix broken test
mreishus Sep 24, 2024
15c51a6
move the collection and block identification logic to the WP_Block_Me…
mreishus Sep 24, 2024
2c3630b
lint fix
mreishus Sep 24, 2024
1fa5877
guard against incorrect paths
mreishus Sep 24, 2024
a4c1455
indent fix
mreishus Sep 24, 2024
26c6b0c
lint fix
mreishus Sep 24, 2024
191c553
Call into WP_Block_Metadata_Registry:: looking for metadata only once…
mreishus Sep 24, 2024
43915a8
add $last_matched_collection
mreishus Sep 24, 2024
abbf5d5
align equals
mreishus Sep 24, 2024
8d7badc
update comments
mreishus Sep 24, 2024
0ca6c93
variable rename
mreishus Sep 25, 2024
70431b2
update comment
mreishus Sep 25, 2024
504e09c
remove private constructor
mreishus Sep 25, 2024
db5dee2
add _doing_it_wrong calls
mreishus Sep 25, 2024
61acb57
fix broken tests
mreishus Sep 25, 2024
4cd8fcc
Remove $identifier_callback option
mreishus Sep 25, 2024
3b13d45
comment updates
mreishus Sep 26, 2024
396e6c9
make default_identifier_callback private
mreishus Sep 26, 2024
78a46fb
standardize _doing_it_wrong messages
mreishus Sep 26, 2024
d948dde
reorganize top of register_block_type_from_metadata
mreishus Sep 26, 2024
15353f4
fix one more since tag
mreishus Sep 26, 2024
a3b73f3
update comment
mreishus Sep 26, 2024
3a5ad69
fix elseif
mreishus Sep 26, 2024
3b70192
default_identifier_callback: Use str_ends_with and return early if empty
mreishus Sep 26, 2024
166ed80
Update src/wp-includes/blocks.php
mreishus Sep 26, 2024
63772f1
Add Tests_Blocks_RegisterBlockTypeFromMetadataWithRegistry
mreishus Sep 26, 2024
b069b60
retab
mreishus Sep 26, 2024
2e44638
add trailing commas
mreishus Sep 26, 2024
779a2e8
align arrows and equals
mreishus Sep 26, 2024
a9d6eb9
let core blocks skip the file_exists check
mreishus Sep 27, 2024
a257e6f
minor change to force test rerun
mreishus Sep 27, 2024
3e4d5b6
Revert "minor change to force test rerun"
mreishus Sep 27, 2024
3a55080
register_collection: Use static variables to reduce calls to wp_norma…
mreishus Sep 28, 2024
6584762
extract to get_include_path and get_plugin_dir functions
mreishus Sep 30, 2024
f2e31d7
allow registering for themes
mreishus Sep 30, 2024
2d7533b
stop someone from registering the entire themes directory
mreishus Sep 30, 2024
498bbba
get_include_path -> get_wpinc_dir
mreishus Sep 30, 2024
c23cf0a
Revert "stop someone from registering the entire themes directory"
mreishus Sep 30, 2024
7fd96b4
Revert "allow registering for themes"
mreishus Sep 30, 2024
9aa134d
Merge branch 'trunk' into add/metadata-register
felixarntz Sep 30, 2024
a323758
Rename wpinc_path to wpinc_dir and clarify docs.
felixarntz Sep 30, 2024
6467b31
Use isset instead of empty.
felixarntz Sep 30, 2024
31809da
Make conditional logic more intuitive.
felixarntz Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}

// 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 ];
}
}
$registry_metadata = WP_Block_Metadata_Registry::get_metadata( $file_or_folder );

// 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'] ) ) ) {
Expand Down
17 changes: 17 additions & 0 deletions src/wp-includes/blocks/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Copy link
Member

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:

function gutenberg_register_core_block_metadata_collection() {
	wp_register_block_metadata_collection(
		BLOCKS_PATH,
		GUTENBERG_BLOCKS_PATH . 'blocks-json.php'
	);
}
remove_action( 'init', 'wp_register_core_block_metadata_collection' );
add_action( 'init', 'gutenberg_register_core_block_metadata_collection', 9 );

Copy link
Member

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? 🤔

BLOCKS_PATH,
BLOCKS_PATH . 'blocks-json.php'
);
}
add_action( 'init', 'wp_register_core_block_metadata_collection', 9 );
273 changes: 273 additions & 0 deletions src/wp-includes/class-wp-block-metadata-registry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<?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 '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
Copy link
Member

Choose a reason for hiding this comment

The 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_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;
}

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 '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;
}
}
1 change: 1 addition & 0 deletions src/wp-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@
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';
Expand Down
Loading
Loading