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 Hooks: Add field to block registration, REST API #5203

Closed
wants to merge 12 commits into from

Conversation

ockham
Copy link
Contributor

@ockham ockham commented Sep 13, 2023

In order to implement Block Hooks (see #59313), we need to add a new block_hooks field to the WP_Block_Type class, as well as to block registration related functions, the block type REST API controller, etc.

Based on #5158.

See #3942 for precedent of adding a new field like this.

Trac ticket: https://core.trac.wordpress.org/ticket/59346


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Comment on lines +709 to +721
'block_hooks' => array(
'description' => __( 'This block is automatically inserted near any occurence of the block types used as keys of this map, into a relative position given by the corresponding value.' ),
'type' => 'object',
'patternProperties' => array(
'^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$' => array(
'type' => 'string',
'enum' => array( 'before', 'after', 'first_child', 'last_child' ),
),
),
'default' => array(),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I don't think that is correct and should changed in gutenberg. This pattern is not needed and make it extremely hard to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See my other comment 😄

@@ -707,15 +707,17 @@ public function get_item_schema() {
'keywords' => $keywords_definition,
'example' => $example_definition,
'block_hooks' => array(
'description' => __( 'Block hooks.' ),
'type' => 'object',
'properties' => array(),
Copy link
Member

Choose a reason for hiding this comment

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

Can't we just use properties. Like this

'instance' => array(
'description' => __( 'Instance settings of the widget, if supported.' ),
'type' => 'object',
'context' => array( 'edit' ),
'default' => null,
'properties' => array(
'encoded' => array(
'description' => __( 'Base64 encoded representation of the instance settings.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'hash' => array(
'description' => __( 'Cryptographic hash of the instance settings.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'raw' => array(
'description' => __( 'Unencoded instance settings, if supported.' ),
'type' => 'object',
'context' => array( 'edit' ),
),
),
),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think so, but maybe I'm mistaken? 🤔

The kind of shape we support is

"blockHooks": {
	"core/comment-template": "lastChild",
	"core/postContent": "after",
}

So the keys can be pretty much any block type, whereas the values can only be one of before, after, firstChild, and lastChild.

It was my understanding that properties is used to define an object with a set of fixed keys -- which we don't really have here. I thus chose patternProperties to allow any block type -- i.e. any alphanumeric string -- as key, while limiting the values to the aforementioned set of possible relative positions.

Copy link
Member

Choose a reason for hiding this comment

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

The way the schema is defined for block.json is at https://github.com/WordPress/gutenberg/blob/40c9f17199b067c75d58f1d918fc26c31b08897f/schemas/json/block.json#L709-L718:

"blockHooks": {
	"description": "Block Hooks allow a block to automatically insert itself next to all instances of a given block type.\n\nSee the Block Hooks documentation at https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#block-hooks-optional for more details.",
	"type": "object",
	"patternProperties": {
		"^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*$": {
			"enum": [ "before", "after", "firstChild", "lastChild" ]
		}
	},
	"additionalProperties": false
},

Copy link
Member

Choose a reason for hiding this comment

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

I see now the comment from @ockham #5203 (comment) that uses:

array(
	'description'       => __( 'This block is automatically inserted near any occurence of the block types used as keys of this map, into a relative position given by the corresponding value.', 'gutenberg' ),
	'patternProperties' => array(
		'^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$' => array(
			'type' => 'string',
			'enum' => array( 'before', 'after', 'first_child', 'last_child' ),
		),
	),
)

It's very close. We should probably unify some aspects. As far as I follow the spec, when using enum, you don't need to define the type, but maybe it's required here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right:

You can use enum even without a type, to accept values of different types. Let’s extend the example to use null to indicate “off”, and also add 42, just for fun.

{
 "enum": ["red", "amber", "green", null, 42]
}

So I don't think the type is required but at least permissible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As for the description, I think it's okay if they're different: After all, in the REST API, we kind of inform the consumer what the field is about:

This block is automatically inserted near any occurence of the block types used as keys of this map, into a relative position given by the corresponding value.

...whereas in the block.json schema, we inform people who write their own block what the block_hooks field is used for:

Block Hooks allow a block to automatically insert itself next to all instances of a given block type.\n\nSee the Block Hooks documentation at https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#block-hooks-optional for more details.

I think what we have now kind of covers each target audience just fine 👍

Copy link
Member

@gziolo gziolo Sep 14, 2023

Choose a reason for hiding this comment

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

Yes, the description can differ, but for example, pattern matching for the block name should always be the same.

By the way, it should be as simple as adding pattern to the name field:

https://json-schema.org/understanding-json-schema/reference/regular_expressions.html#example

@ockham
Copy link
Contributor Author

ockham commented Sep 14, 2023

Rebased.

@ockham ockham marked this pull request as ready for review September 14, 2023 10:22
Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

That should cover adding the block_hooks field to WP_Block_Type and expose it in the REST API endpoint for block types.

Let's continue discussing the shape for the schema after landing initial changes. It's a more complex case as pointed out by @ockham, because the key is the block name that can be anything that matches the pattern for the block name. If that helps we could include the pattern also for the name field:

'name' => array(
'description' => __( 'Unique name identifying the block type.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),

I see there is custom logic for mapping blockHooks to block_hooks. I'd like to work on a follow-up to cover that part with unit tests. before case is covered indirectly by other values still could use some testing.

One last bit of feedback is for the _doing_it_wrong logic. I like that there is additional security enforced. I'm wondering if we should move it to the WP_Block_Type class though, to cover also the case when someones registers the block with:

register_block_type( 'my-plugin/my-block', array(
    'block_hooks' => array(
        'my-plugin/my-block': 'first_child',
    ),
) );

It also reminds me that in general there isn't that much sanitization present on the code for settings provided during block registration which I find a bit unfortunate.


There is nothing blocking this PR, and we can iterate on the items I listed if they make sense.

@ockham
Copy link
Contributor Author

ockham commented Sep 14, 2023

Thank you @gziolo!

[...] the key is the block name that can be anything that matches the pattern for the block name. If that helps we could include the pattern also for the name field:

'name' => array(
'description' => __( 'Unique name identifying the block type.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),

That reminded me of something else; maybe we can cross-reference the name field in the block_hooks schema definition? Would make it more semantic -- we'd basically state, "The keys in block_hooks are block names.".

I see there is custom logic for mapping blockHooks to block_hooks. I'd like to work on a follow-up to cover that part with unit tests. before case is covered indirectly by other values still could use some testing.

Ah yeah, that's a fair point; especially for the camelCase -> snake_case mappings.

One last bit of feedback is for the _doing_it_wrong logic. I like that there is additional security enforced. I'm wondering if we should move it to the WP_Block_Type class though, to cover also the case when someones registers the block with:

register_block_type( 'my-plugin/my-block', array(
    'block_hooks' => array(
        'my-plugin/my-block': 'first_child',
    ),
) );

FWIW, register_block_type actually calls register_block_type_from_metadata for block registration, so your example should already be covered:

function register_block_type( $block_type, $args = array() ) {
if ( is_string( $block_type ) && file_exists( $block_type ) ) {
return register_block_type_from_metadata( $block_type, $args );
}
return WP_Block_Type_Registry::get_instance()->register( $block_type, $args );
}

(We might want to add some unit test for this too, BTW 🤔 )

It also reminds me that in genera; there isn't that much sanitization present on the code for settings provided during block registration which I find a bit unforunate.

Yeah, good point!

@ockham
Copy link
Contributor Author

ockham commented Sep 14, 2023

Committed to Core in https://core.trac.wordpress.org/changeset/56587.

@ockham ockham closed this Sep 14, 2023
@ockham ockham deleted the add/block-hooks-field branch September 14, 2023 13:24
'description' => __( 'This block is automatically inserted near any occurence of the block types used as keys of this map, into a relative position given by the corresponding value.' ),
'type' => 'object',
'patternProperties' => array(
'^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$' => array(
Copy link
Member

Choose a reason for hiding this comment

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

@ockham How something like this.

$properties = array();
$registry   = WP_Block_Type_Registry::get_instance();
foreach( $registry->get_all_registered() as $block ){
   $properties[ $block ] = array(
       'type' => 'string',
       'enum' => array( 'before', 'after', 'first_child', 'last_child' ),
   );
}

Copy link
Member

Choose a reason for hiding this comment

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

Noting that there might be hundreds of registered blocks, but the block author might still list the block type name that isn't present on the site and we would consider it valid. What's the reasoning behind listing all available blocks?

@gziolo
Copy link
Member

gziolo commented Sep 14, 2023

In #5218 I added some refactorings discussed above:

  • Block Hooks: Add field to block registration, REST API #5203 (comment) - gutenberg text domain remove
  • There is custom logic for mapping blockHooks to block_hooks when handling metadata. I covered that part in unit tests.
  • One last bit of feedback was for the _doing_it_wrong logic. I moved the check to the WP_Block_Type class to cover also the case when someone registers the block with a regular register_block_type call without using metadata file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants