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

Block Bindings API: Refactor logic into Block Bindings class and singleton pattern #57742

Merged
merged 16 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 69 additions & 0 deletions lib/experimental/block-bindings/block-bindings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php
/**
* Block Bindings API
*
* This file contains functions for managing block bindings in WordPress.
*
* @since 17.6.0
* @package gutenberg
*/

/**
* Retrieves the singleton instance of WP_Block_Bindings.
*
* @return WP_Block_Bindings The WP_Block_Bindings instance.
*/
if ( ! function_exists( 'wp_block_bindings' ) ) {
function wp_block_bindings() {
static $instance = null;
if ( is_null( $instance ) ) {
$instance = new WP_Block_Bindings();
}
return $instance;
}
}

/**
* Registers a new source for block bindings.
*
* @param string $source_name The name of the source.
* @param string $label The label of the source.
* @param callable $apply The callback executed when the source is processed during block rendering. The callable should have the following signature:
* function (object $source_attrs, object $block_instance, string $attribute_name): string
* - object $source_attrs: Object containing source ID used to look up the override value, i.e. {"value": "{ID}"}.
* - object $block_instance: The block instance.
* - string $attribute_name: The name of an attribute used to retrieve an override value from the block context.
* The callable should return a string that will be used to override the block's original value.
* @return void
*/
if ( ! function_exists( 'wp_block_bindings_register_source' ) ) {
function wp_block_bindings_register_source( $source_name, $label, $apply ) {
wp_block_bindings()->register_source( $source_name, $label, $apply );
}
}

/**
* Retrieves the list of registered block sources.
*
* @return array The list of registered block sources.
*/
if ( ! function_exists( 'wp_block_bindings_get_sources' ) ) {
function wp_block_bindings_get_sources() {
return wp_block_bindings()->get_sources();
}
}

/**
* Replaces the HTML content of a block based on the provided source value.
*
* @param string $block_content Block Content.
* @param string $block_name The name of the block to process.
* @param string $block_attr The attribute of the block we want to process.
* @param string $source_value The value used to replace the HTML.
* @return string The modified block content.
*/
if ( ! function_exists( 'wp_block_bindings_replace_html' ) ) {
function wp_block_bindings_replace_html( $block_content, $block_name, $block_attr, $source_value ) {
return wp_block_bindings()->replace_html( $block_content, $block_name, $block_attr, $source_value );
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,50 @@
<?php
/**
* Define the mechanism to replace the HTML depending on the block attributes.
* Block Bindings API: WP_Block_Bindings class.
*
* Support for overriding content in blocks by connecting them to different sources.
*
* @since 17.6.0
* @package gutenberg
*/

if ( ! function_exists( 'block_bindings_replace_html' ) ) {
if ( class_exists( 'WP_Block_Bindings' ) ) {
return;
}

/**
* Core class used to define supported blocks, register sources, and populate HTML with content from those sources.
*/
class WP_Block_Bindings {

/**
* Holds the registered block bindings sources, keyed by source identifier.
*
* @var array
*/
private $sources = array();

/**
* Function to register a new source.
*
* @param string $source_name The name of the source.
* @param string $label The label of the source.
* @param callable $apply The callback executed when the source is processed during block rendering. The callable should have the following signature:
* function (object $source_attrs, object $block_instance, string $attribute_name): string
* - object $source_attrs: Object containing source ID used to look up the override value, i.e. {"value": "{ID}"}.
* - object $block_instance: The block instance.
* - string $attribute_name: The name of an attribute used to retrieve an override value from the block context.
* The callable should return a string that will be used to override the block's original value.
*
* @return void
*/
public function register_source( $source_name, $label, $apply ) {
$this->sources[ $source_name ] = array(
'label' => $label,
'apply' => $apply,
);
}

/**
* Depending on the block attributes, replace the proper HTML based on the value returned by the source.
*
Expand All @@ -14,7 +53,7 @@
* @param string $block_attr The attribute of the block we want to process.
* @param string $source_value The value used to replace the HTML.
*/
function block_bindings_replace_html( $block_content, $block_name, $block_attr, $source_value ) {
public function replace_html( $block_content, $block_name, $block_attr, $source_value ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name );
if ( null === $block_type ) {
return;
Expand Down Expand Up @@ -107,4 +146,13 @@ function block_bindings_replace_html( $block_content, $block_name, $block_attr,
}
return;
}

/**
* Retrieves the list of registered block sources.
*
* @return array The array of registered sources.
*/
public function get_sources() {
return $this->sources;
}
}
5 changes: 2 additions & 3 deletions lib/experimental/block-bindings/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
* @package gutenberg
*/

require_once __DIR__ . '/sources/index.php';
require_once __DIR__ . '/html-processing.php';

require_once __DIR__ . '/class-wp-block-bindings.php';
require_once __DIR__ . '/block-bindings.php';
// Register the sources.
$gutenberg_experiments = get_option( 'gutenberg-experiments' );
if ( $gutenberg_experiments ) {
Expand Down
32 changes: 0 additions & 32 deletions lib/experimental/block-bindings/sources/index.php

This file was deleted.

5 changes: 2 additions & 3 deletions lib/experimental/block-bindings/sources/pattern.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
*
* @package gutenberg
*/

if ( function_exists( 'register_block_bindings_source' ) ) {
if ( function_exists( 'wp_block_bindings_register_source' ) ) {
$pattern_source_callback = function ( $source_attrs, $block_instance, $attribute_name ) {
if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) {
return null;
}
$block_id = $block_instance->attributes['metadata']['id'];
return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null );
};
register_block_bindings_source(
wp_block_bindings_register_source(
'pattern_attributes',
__( 'Pattern Attributes', 'gutenberg' ),
$pattern_source_callback
Expand Down
5 changes: 2 additions & 3 deletions lib/experimental/block-bindings/sources/post-meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
*
* @package gutenberg
*/

if ( function_exists( 'register_block_bindings_source' ) ) {
if ( function_exists( 'wp_block_bindings_register_source' ) ) {
$post_meta_source_callback = function ( $source_attrs ) {
// Use the postId attribute if available
if ( isset( $source_attrs['postId'] ) ) {
Expand All @@ -17,7 +16,7 @@

return get_post_meta( $post_id, $source_attrs['value'], true );
};
register_block_bindings_source(
wp_block_bindings_register_source(
'post_meta',
__( 'Post Meta', 'gutenberg' ),
$post_meta_source_callback
Expand Down
48 changes: 22 additions & 26 deletions lib/experimental/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,26 +88,28 @@ function wp_enqueue_block_view_script( $block_name, $args ) {
) ) {

require_once __DIR__ . '/block-bindings/index.php';
// Allowed blocks that support block bindings.
// TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes?
global $block_bindings_allowed_blocks;
$block_bindings_allowed_blocks = array(
'core/paragraph' => array( 'content' ),
'core/heading' => array( 'content' ),
'core/image' => array( 'url', 'title', 'alt' ),
'core/button' => array( 'url', 'text' ),
);
if ( ! function_exists( 'process_block_bindings' ) ) {

if ( ! function_exists( 'gutenberg_process_block_bindings' ) ) {
/**
* Process the block bindings attribute.
*
* @param string $block_content Block Content.
* @param array $block Block attributes.
* @param WP_Block $block_instance The block instance.
*/
function process_block_bindings( $block_content, $block, $block_instance ) {
// If the block doesn't have the bindings property, return.
if ( ! isset( $block['attrs']['metadata']['bindings'] ) ) {
function gutenberg_process_block_bindings( $block_content, $block, $block_instance ) {

// Allowed blocks that support block bindings.
// TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes?
$allowed_blocks = array(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a similar check in block-supports/pattern.php as seen here: https://github.com/WordPress/gutenberg/pull/57789/files#r1452055620. Do you think we should duplicate this array there? Or should we move this into a sharable JSON config so that it can also be imported by JS? Not sure if it's worth the effort though since it's supposed to be a temporary solution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think question number one is whether these two lists should always match. Eventually, it should be possible to use block bindings with all blocks and I expect to see a method to opt in for that. The prerequisite is the full support in HTML API to replace HTML tags in the saved content.

Copy link
Contributor Author

@artemiomorales artemiomorales Jan 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, where would we put the JSON config? My initial reaction is that it's not worth the effort because this is a temporary solution. The temporary solution may be in place for a while, though.

If we do add a JSON config, that means we'd need to make sure support for every block works for both regular blocks and patterns moving forward. To @gziolo's point, that would require more coordination and perhaps may be less productive at this stage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was basically tossing out three different solutions above 😅:

  1. Duplicate the $allowed_blocks config in block-supports/pattern.php.
  2. Move the constant to a sharable PHP file that can be imported by both PHP scripts.
  3. Go a bit further and move the constant to a sharable JSON file that can also be imported by client-side JS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think duplicating it is ok for now.

I've put together a PR that does that - Update pattern overrides to use a hard coded support array

It should ensure pattern overrides don't break when this PR is merged (I think this PR is currently a little bit behind trunk).

'core/paragraph' => array( 'content' ),
'core/heading' => array( 'content' ),
'core/image' => array( 'url', 'title', 'alt' ),
'core/button' => array( 'url', 'text' ),
);

// If the block doesn't have the bindings property or isn't one of the allowed block types, return.
if ( ! isset( $block['attrs']['metadata']['bindings'] ) || ! isset( $allowed_blocks[ $block_instance->name ] ) ) {
return $block_content;
}

Expand All @@ -128,16 +130,13 @@ function process_block_bindings( $block_content, $block, $block_instance ) {
// }
// }
//
global $block_bindings_allowed_blocks;
global $block_bindings_sources;

$block_bindings_sources = wp_block_bindings_get_sources();
$modified_block_content = $block_content;
foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) {
// If the block is not in the list, stop processing.
if ( ! isset( $block_bindings_allowed_blocks[ $block['blockName'] ] ) ) {
return $block_content;
}

// If the attribute is not in the list, process next attribute.
if ( ! in_array( $binding_attribute, $block_bindings_allowed_blocks[ $block['blockName'] ], true ) ) {
if ( ! in_array( $binding_attribute, $allowed_blocks[ $block_instance->name ], true ) ) {
continue;
}
// If no source is provided, or that source is not registered, process next attribute.
Expand All @@ -159,14 +158,11 @@ function process_block_bindings( $block_content, $block, $block_instance ) {
}

// Process the HTML based on the block and the attribute.
$modified_block_content = block_bindings_replace_html( $modified_block_content, $block['blockName'], $binding_attribute, $source_value );
$modified_block_content = wp_block_bindings_replace_html( $modified_block_content, $block_instance->name, $binding_attribute, $source_value );
}
return $modified_block_content;
}

// Add filter only to the blocks in the list.
foreach ( $block_bindings_allowed_blocks as $block_name => $attributes ) {
add_filter( 'render_block_' . $block_name, 'process_block_bindings', 20, 3 );
}
}

add_filter( 'render_block', 'gutenberg_process_block_bindings', 20, 3 );
}
Loading