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: Add block uses_context defined by block bindings sources #6079

Closed
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
63d381f
Read `uses_context` defined by sources
SantosGuillamot Feb 13, 2024
65e259a
Add `uses_context` to post meta source
SantosGuillamot Feb 13, 2024
68ce2a8
Change post meta `$post_id`
SantosGuillamot Feb 13, 2024
ade4119
Use `uses_context` in pattern overrides source
SantosGuillamot Feb 13, 2024
5011e16
Fix formatting
SantosGuillamot Feb 13, 2024
929afb6
Move logic to source registration
SantosGuillamot Feb 13, 2024
44125a4
Format latest changes
SantosGuillamot Feb 13, 2024
5a931f4
Remove empty line
SantosGuillamot Feb 13, 2024
dccef2e
Revert "Use `uses_context` in pattern overrides source"
SantosGuillamot Feb 13, 2024
016ec29
Add `uses_context` to pattern overrides source
SantosGuillamot Feb 13, 2024
e305f7c
Create `get_block_type_uses_context` filter
SantosGuillamot Feb 13, 2024
49a8b7a
Use `get_block_type_uses_context` in source registration
SantosGuillamot Feb 13, 2024
42575c4
Update docstrings
SantosGuillamot Feb 13, 2024
e41ba30
Move supported blocks to private variables
SantosGuillamot Feb 13, 2024
4ca8176
Remove unnecessary attributes from variable
SantosGuillamot Feb 13, 2024
46df776
Change `$support_blocks` comment
SantosGuillamot Feb 13, 2024
59746c0
Fix typo in comment
SantosGuillamot Feb 13, 2024
7bcc71e
Check the `uses_context` property is an array
SantosGuillamot Feb 13, 2024
5810180
Fix process bindings check
SantosGuillamot Feb 13, 2024
a578d5c
Use strict argument in `in_array`
SantosGuillamot Feb 13, 2024
9f01568
Use `empty` function in post meta source
SantosGuillamot Feb 13, 2024
ba7bf25
Update comments and descriptions
SantosGuillamot Feb 13, 2024
0c4a795
Remove variations and uses_context in `__set` function
SantosGuillamot Feb 14, 2024
7e80f5e
Add test to check if the value of the source context shows
SantosGuillamot Feb 15, 2024
d839f33
Test if the source result object properties are correct
SantosGuillamot Feb 15, 2024
e82879e
Test registration fails when `uses_context` is string
SantosGuillamot Feb 15, 2024
102c940
Test merging multiple sources `uses_context`
SantosGuillamot Feb 15, 2024
f4af4f7
Join variations and uses_context in the same conditional
SantosGuillamot Feb 15, 2024
f2b3b3a
Move multiple sources test to a different file
SantosGuillamot Feb 15, 2024
4f8668d
Provide error message in uses_context test
SantosGuillamot Feb 15, 2024
07a7317
Check allowed source properties in the registry
SantosGuillamot Feb 15, 2024
f14817f
Remove register_meta code
SantosGuillamot Feb 15, 2024
edaaffa
Remove spaces in $block_content in tests
SantosGuillamot Feb 15, 2024
03e66ee
Check that `uses_context` increase by three
SantosGuillamot Feb 15, 2024
0b49cc4
Update comment to use third person
SantosGuillamot Feb 15, 2024
9838fec
Don't use $merged_uses_context variable
SantosGuillamot Feb 15, 2024
59fde29
Change translators comment syntax to match standard
SantosGuillamot Feb 15, 2024
5f2ddc3
Add covers get_uses_context
SantosGuillamot Feb 15, 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
25 changes: 13 additions & 12 deletions src/wp-includes/block-bindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,19 @@
* @param array $source_properties {
* The array of arguments that are used to register a source.
*
* @type string $label The label of the source.
* @type callback $get_value_callback A callback executed when the source is processed during block rendering.
* The callback should have the following signature:
*
* `function ($source_args, $block_instance,$attribute_name): mixed`
* - @param array $source_args Array containing source arguments
* used to look up the override value,
* i.e. {"key": "foo"}.
* - @param WP_Block $block_instance The block instance.
* - @param string $attribute_name The name of an attribute .
* The callback has a mixed return type; it may return a string to override
* the block's original value, null, false to remove an attribute, etc.
* @type string $label The label of the source.
* @type callback $get_value_callback A callback executed when the source is processed during block rendering.
* The callback should have the following signature:
*
* `function ($source_args, $block_instance,$attribute_name): mixed`
* - @param array $source_args Array containing source arguments
* used to look up the override value,
* i.e. {"key": "foo"}.
* - @param WP_Block $block_instance The block instance.
* - @param string $attribute_name The name of an attribute .
* The callback has a mixed return type; it may return a string to override
* the block's original value, null, false to remove an attribute, etc.
* @type array $uses_context (optional) Array of values to add to block `uses_context` needed by the source.
* }
* @return WP_Block_Bindings_Source|false Source when the registration was successful, or `false` on failure.
*/
Expand Down
1 change: 1 addition & 0 deletions src/wp-includes/block-bindings/pattern-overrides.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function _register_block_bindings_pattern_overrides_source() {
array(
'label' => _x( 'Pattern Overrides', 'block bindings source' ),
'get_value_callback' => '_block_bindings_pattern_overrides_get_value',
'uses_context' => array( 'pattern/overrides' ),
)
);
}
Expand Down
19 changes: 9 additions & 10 deletions src/wp-includes/block-bindings/post-meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,20 @@
* @since 6.5.0
* @access private
*
* @param array $source_args Array containing source arguments used to look up the override value.
* Example: array( "key" => "foo" ).
* @param array $source_args Array containing source arguments used to look up the override value.
* Example: array( "key" => "foo" ).
* @param WP_Block $block_instance The block instance.
* @return mixed The value computed for the source.
gziolo marked this conversation as resolved.
Show resolved Hide resolved
*/
function _block_bindings_post_meta_get_value( array $source_args ) {
if ( ! isset( $source_args['key'] ) ) {
function _block_bindings_post_meta_get_value( array $source_args, $block_instance ) {
if ( empty( $source_args['key'] ) ) {
return null;
}

// Use the postId attribute if available.
if ( isset( $source_args['postId'] ) ) {
$post_id = $source_args['postId'];
} else {
// $block_instance->context['postId'] is not available in the Image block.
$post_id = get_the_ID();
if ( empty( $block_instance->context['postId'] ) ) {
return null;
}
$post_id = $block_instance->context['postId'];

// If a post isn't public, we need to prevent unauthorized users from accessing the post meta.
$post = get_post( $post_id );
Expand All @@ -51,6 +49,7 @@ function _register_block_bindings_post_meta_source() {
array(
'label' => _x( 'Post Meta', 'block bindings source' ),
'get_value_callback' => '_block_bindings_post_meta_get_value',
'uses_context' => array( 'postId', 'postType' ),
gziolo marked this conversation as resolved.
Show resolved Hide resolved
)
);
}
Expand Down
74 changes: 57 additions & 17 deletions src/wp-includes/class-wp-block-bindings-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ final class WP_Block_Bindings_Registry {
*/
private static $instance = null;

/**
* Supported blocks that can use the block bindings API.
*
* @since 6.5.0
* @var array
*/
private $supported_blocks = array(
'core/paragraph',
'core/heading',
'core/image',
'core/button',
);

/**
* Registers a new block bindings source.
*
Expand All @@ -53,18 +66,19 @@ final class WP_Block_Bindings_Registry {
* @param array $source_properties {
* The array of arguments that are used to register a source.
*
* @type string $label The label of the source.
* @type callback $get_value_callback A callback executed when the source is processed during block rendering.
* The callback should have the following signature:
*
* `function ($source_args, $block_instance,$attribute_name): mixed`
* - @param array $source_args Array containing source arguments
* used to look up the override value,
* i.e. {"key": "foo"}.
* - @param WP_Block $block_instance The block instance.
* - @param string $attribute_name The name of the target attribute.
* The callback has a mixed return type; it may return a string to override
* the block's original value, null, false to remove an attribute, etc.
* @type string $label The label of the source.
* @type callback $get_value_callback A callback executed when the source is processed during block rendering.
* The callback should have the following signature:
*
* `function ($source_args, $block_instance,$attribute_name): mixed`
* - @param array $source_args Array containing source arguments
* used to look up the override value,
* i.e. {"key": "foo"}.
* - @param WP_Block $block_instance The block instance.
* - @param string $attribute_name The name of the target attribute.
* The callback has a mixed return type; it may return a string to override
* the block's original value, null, false to remove an attribute, etc.
* @type array $uses_context (optional) Array of values to add to block `uses_context` needed by the source.
* }
* @return WP_Block_Bindings_Source|false Source when the registration was successful, or `false` on failure.
*/
Expand Down Expand Up @@ -100,14 +114,14 @@ public function register( string $source_name, array $source_properties ) {
if ( $this->is_registered( $source_name ) ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s: Block bindings source name. */
// translators: %s: Block bindings source name.
sprintf( __( 'Block bindings source "%s" already registered.' ), $source_name ),
'6.5.0'
);
return false;
}

/* Validate that the source properties contain the label */
// Validate that the source properties contain the label.
if ( ! isset( $source_properties['label'] ) ) {
_doing_it_wrong(
__METHOD__,
Expand All @@ -117,7 +131,7 @@ public function register( string $source_name, array $source_properties ) {
return false;
}

/* Validate that the source properties contain the get_value_callback */
// Validate that the source properties contain the get_value_callback.
if ( ! isset( $source_properties['get_value_callback'] ) ) {
_doing_it_wrong(
__METHOD__,
Expand All @@ -127,7 +141,7 @@ public function register( string $source_name, array $source_properties ) {
return false;
}

/* Validate that the get_value_callback is a valid callback */
// Validate that the get_value_callback is a valid callback.
if ( ! is_callable( $source_properties['get_value_callback'] ) ) {
_doing_it_wrong(
__METHOD__,
Expand All @@ -137,13 +151,39 @@ public function register( string $source_name, array $source_properties ) {
return false;
}

// Validate that the uses_context parameter is an array.
if ( isset( $source_properties['uses_context'] ) && ! is_array( $source_properties['uses_context'] ) ) {
_doing_it_wrong(
__METHOD__,
__( 'The "uses_context" parameter must be an array.' ),
'6.5.0'
);
return false;
}

$source = new WP_Block_Bindings_Source(
$source_name,
$source_properties
);

$this->sources[ $source_name ] = $source;

// Add `uses_context` defined by block bindings sources.
add_filter(
'get_block_type_uses_context',
function ( $uses_context, $block_type ) use ( $source ) {
Copy link
Member

@gziolo gziolo Feb 15, 2024

Choose a reason for hiding this comment

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

One more thing here and not a blocker. I believe we should use a class method and unregister the filter when unregistering the source. It would also help with documenting, as we could put all details from inline comments in the PHPDoc.

I'm not entirely sure how that would look in practice, but happy to explore together options. It can be another patch.

Copy link
Author

Choose a reason for hiding this comment

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

This makes sense to me. Although I don't know how it will work 🤔 I was thinking something like this:

	/**
	 * Source that is being processed.
	 *
	 * @since 6.5.0
	 * @var WP_Block_Bindings_Source|false
	 */
	private $source;

	/**
	 * Callback passed to the `get_block_type_uses_context` filter to add the source's `uses_context` to the block.
	 *
	 * @since 6.5.0
	 *
	 * @param array         $uses_context Array of registered uses context for a block type.
	 * @param WP_Block_Type $block_type   The full block type object.
	 * @return array Modified array of uses_context with the values provided by the source.
	 */
	public function _add_source_uses_context( $uses_context, $block_type ) {
		$source = $this->source;
		if ( ! in_array( $block_type->name, $this->supported_blocks, true ) || empty( $source->uses_context ) ) {
			return $uses_context;
		}
		// Use array_values to reset the array keys.
		return array_values( array_unique( array_merge( $uses_context, $source->uses_context ) ) );
	}

But we probably need a different callback name per source.

Anyway, I'll add a task in the tracking issue to handle it 🙂

if ( ! in_array( $block_type->name, $this->supported_blocks, true ) || empty( $source->uses_context ) ) {
return $uses_context;
}
// Use array_values to reset the array keys.
$merged_uses_context = array_values( array_unique( array_merge( $uses_context, $source->uses_context ) ) );

return $merged_uses_context;
SantosGuillamot marked this conversation as resolved.
Show resolved Hide resolved
},
10,
2
);

return $source;
}

Expand All @@ -159,7 +199,7 @@ public function unregister( string $source_name ) {
if ( ! $this->is_registered( $source_name ) ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s: Block bindings source name. */
// translators: %s: Block bindings source name.
SantosGuillamot marked this conversation as resolved.
Show resolved Hide resolved
sprintf( __( 'Block binding "%s" not found.' ), $source_name ),
'6.5.0'
);
Expand Down
11 changes: 11 additions & 0 deletions src/wp-includes/class-wp-block-bindings-source.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ final class WP_Block_Bindings_Source {
*/
private $get_value_callback;

/**
* The context added to the blocks needed by the source.
*
* @since 6.5.0
* @var array|null
*/
public $uses_context = null;

/**
* Constructor.
*
Expand All @@ -60,6 +68,9 @@ public function __construct( string $name, array $source_properties ) {
$this->name = $name;
$this->label = $source_properties['label'];
$this->get_value_callback = $source_properties['get_value_callback'];
if ( isset( $source_properties['uses_context'] ) ) {
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 that we should move this isset() check next to the other checks
in WP_Block_Bindings_Registry.

Copy link
Contributor

Choose a reason for hiding this comment

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

And let's cover it with a unit test as well
🙂. (Should be almost a copy-paste of the
existing ones)

Copy link
Author

Choose a reason for hiding this comment

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

My idea is to make the uses_context optional because some sources won't need to use it. Do you think it shouldn't be that case? It would make sense to add a check that, if it exists, it is an array though.

I'll work on unit tests as well 🙂

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it should be optional and default to null.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, that makes sense! It should be optional indeed 🙂

Copy link
Author

Choose a reason for hiding this comment

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

If I understood you correctly, I believe I prefer the second option. I made this commit to illustrate how I understood it. We can always revert it.

PD: I included unnecessary code I was using for testing, I just removed it in other commit 😅

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it's exactly how it could work 👍🏻

It would be nice to add a unit test for the registry that asserts that it errors when an unknown property is provided.


At the same time, we are closing the source object for an extension, which I'm a bit unsure about, given we might need to change during beta if we figure out it prevents iterations in the Gutenberg plugin. We also miss the following annotation on the class if we decide to go that path:

Copy link
Author

Choose a reason for hiding this comment

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

I am unsure about restricting it as well. I can expect we might want to increase it in Gutenberg after external developers create their own sources the same way we need now to include uses_context.

Copy link
Author

Choose a reason for hiding this comment

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

On the other hand, if we allow source to use their own properties, could we face backwards-compatibility issues in the future?

Imagine a source is using a custom property format. And in the future, we want to include it as part of the core properties and we use it somehow as we are doing with uses_context. The source that had a custom property with the same name wouldn't be working as expected, right?

Copy link
Member

Choose a reason for hiding this comment

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

I'll keep thinking about it as I don't have an ultimate answer for now as it's hard to tell what will be needed here.

$this->uses_context = $source_properties['uses_context'];
}
}

/**
Expand Down
32 changes: 25 additions & 7 deletions src/wp-includes/class-wp-block-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ class WP_Block_Type {
* @since 5.5.0
* @var string[]
*/
public $uses_context = array();
private $uses_context = array();

/**
* Context provided by blocks of this type.
Expand Down Expand Up @@ -366,6 +366,10 @@ public function __get( $name ) {
return $this->get_variations();
}

if ( 'uses_context' === $name ) {
return $this->get_uses_context();
}

if ( ! in_array( $name, $this->deprecated_properties, true ) ) {
return;
}
Expand Down Expand Up @@ -394,7 +398,7 @@ public function __get( $name ) {
* or false otherwise.
*/
public function __isset( $name ) {
if ( 'variations' === $name ) {
if ( in_array( $name, array( 'variations', 'uses_context' ), true ) ) {
return true;
}

Expand All @@ -417,11 +421,6 @@ public function __isset( $name ) {
* @param mixed $value Property value.
*/
public function __set( $name, $value ) {
if ( 'variations' === $name ) {
$this->variations = $value;
return;
}

if ( ! in_array( $name, $this->deprecated_properties, true ) ) {
$this->{$name} = $value;
return;
Expand Down Expand Up @@ -616,4 +615,23 @@ public function get_variations() {
*/
return apply_filters( 'get_block_type_variations', $this->variations, $this );
}

/**
* Get block uses context.
*
* @since 6.5.0
*
* @return array[]
*/
public function get_uses_context() {
/**
* Filters the registered uses context for a block type.
*
* @since 6.5.0
*
* @param array $uses_context Array of registered uses context for a block type.
* @param WP_Block_Type $block_type The full block type object.
*/
return apply_filters( 'get_block_type_uses_context', $this->uses_context, $this );
}
}
12 changes: 5 additions & 7 deletions src/wp-includes/class-wp-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,28 +235,26 @@ private function process_block_bindings() {

$computed_attributes = array();

// 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(
$supported_block_attrs = array(
'core/paragraph' => array( 'content' ),
'core/heading' => array( 'content' ),
'core/image' => array( 'url', 'title', 'alt' ),
'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ),
);

// If the block doesn't have the bindings property, isn't one of the allowed
// If the block doesn't have the bindings property, isn't one of the supported
// block types, or the bindings property is not an array, return the block content.
if (
! isset( $allowed_blocks[ $this->name ] ) ||
! isset( $supported_block_attrs[ $this->name ] ) ||
empty( $parsed_block['attrs']['metadata']['bindings'] ) ||
! is_array( $parsed_block['attrs']['metadata']['bindings'] )
) {
return $computed_attributes;
}

foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) {
// If the attribute is not in the allowed list, process next attribute.
if ( ! in_array( $attribute_name, $allowed_blocks[ $this->name ], true ) ) {
// If the attribute is not in the supported list, process next attribute.
if ( ! in_array( $attribute_name, $supported_block_attrs[ $this->name ], true ) ) {
continue;
}
// If no source is provided, or that source is not registered, process next attribute.
Expand Down
43 changes: 43 additions & 0 deletions tests/phpunit/tests/block-bindings/render.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,49 @@ public function test_passing_arguments_to_source() {
);
}

/**
* Test passing `uses_context` as argument to the source.
*
* @ticket 60525
*
* @covers ::register_block_bindings_source
*/
public function test_passing_uses_context_to_source() {
$get_value_callback = function ( $source_args, $block_instance, $attribute_name ) {
$value = $block_instance->context['sourceContext'];
return "Value: $value";
};

register_block_bindings_source(
self::SOURCE_NAME,
array(
'label' => self::SOURCE_LABEL,
'get_value_callback' => $get_value_callback,
'uses_context' => array( 'sourceContext' ),
)
);

$block_content = <<<HTML
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"test/source", "args": {"key": "test"}}}}} -->
<p>This should not appear</p>
<!-- /wp:paragraph -->
HTML;
$parsed_blocks = parse_blocks( $block_content );
gziolo marked this conversation as resolved.
Show resolved Hide resolved
$block = new WP_Block( $parsed_blocks[0], array( 'sourceContext' => 'source context value' ) );
$result = $block->render();

$this->assertSame(
'Value: source context value',
$block->attributes['content'],
'Value: source context value'
);
$this->assertSame(
'<p>Value: source context value</p>',
trim( $result ),
'Value: source context value'
gziolo marked this conversation as resolved.
Show resolved Hide resolved
);
}

/**
* Tests if the block content is updated with the value returned by the source
* for the Image block in the placeholder state.
Expand Down
Loading
Loading