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

render_block_context another approach that solve the context issue #7522

Open
wants to merge 37 commits into
base: trunk
Choose a base branch
from

Conversation

mukeshpanchal27
Copy link
Member

@mukeshpanchal27 mukeshpanchal27 commented Oct 7, 2024

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


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.

Copy link

github-actions bot commented Oct 7, 2024

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@dlh01
Copy link

dlh01 commented Oct 7, 2024

@mukeshpanchal27 Heads up that the tests in #7344 have been updated to reflect the last few comments there.

@gziolo
Copy link
Member

gziolo commented Oct 8, 2024

Nice one @mukeshpanchal27. @dlh01 and @joemcgill, it looks like all the unit tests we could think of pass in this branch. How do you feel about the proposed solution? Is it good enough to move forward as part of the WP 6.7 release?

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

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

@mukeshpanchal27 This looks very promising. Mostly questions from my end, to make sure I understand this correctly.

src/wp-includes/class-wp-block.php Outdated Show resolved Hide resolved
src/wp-includes/class-wp-block.php Outdated Show resolved Hide resolved
src/wp-includes/class-wp-block.php Outdated Show resolved Hide resolved
src/wp-includes/class-wp-block.php Outdated Show resolved Hide resolved
@dlh01
Copy link

dlh01 commented Oct 9, 2024

Thanks for the ping! I should be able to take a look at this Wednesday or Thursday.

@mukeshpanchal27 mukeshpanchal27 changed the title Testing: render_block_context another approach render_block_context another approach that solve the context issue Oct 9, 2024
@mukeshpanchal27
Copy link
Member Author

The PR is ready for review. Address the feedbacks.

@gziolo gziolo marked this pull request as ready for review October 9, 2024 09:46
Copy link

github-actions bot commented Oct 9, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props mukesh27, flixos90, gziolo, dlh, joemcgill, santosguillamot.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@mukeshpanchal27
Copy link
Member Author

I take another pass and try to find the the solution to not merge the $this->available_context to $this->context but somehow it break the old or new unit tests. Could anyone take a look when they you have moment?

@mukeshpanchal27
Copy link
Member Author

@felixarntz @joemcgill @dlh01 @gziolo

In c83ca82, I removed the context merging from $this->available_context to $this->context and reverted the unit test changes.

Currently, there’s only one uncovered case: when a block needs context for itself, not for nested blocks. The test_render_block_context_allowed_context() test partially covers this—the arbitrary context isn't available for a single block but works correctly for nested blocks.

In the single block case, the arbitrary context is in available_context but not in context. To address this, we could either merge contexts or apply the render_block_context filter to context. However, the latter would impact existing unit tests.

Would it be acceptable to remove the failing unit test and merge this PR, as it resolves the ancestor block context issue? We can then open a follow-up discussion to address the remaining part.

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

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

@mukeshpanchal27 This looks almost there for me, just one note.

src/wp-includes/class-wp-block.php Outdated Show resolved Hide resolved
Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

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

@mukeshpanchal27 This looks solid. Regarding your comment #7522 (comment), sharing my thoughts here.

TL;DR: The main problem we still need to address is that render_block_context effectively filters a block's $context for inner blocks, but a block's $available_context for top-level blocks.

First of all, there's definitely some inconsistency today between the filter application, which is part of the problem:

  • In WP_Block::render(), the render_block_data and render_block_context filters are applied after the block has been instantiated.
  • In render_block however the two filters apply before the block is instantiated, leading to different behavior that causes the test to fail.

So far, so good. I think the test_render_block_context_allowed_context() test may not be entirely right in what it tests though, since we also shouldn't ignore the uses_context property of a block. Not all available_context should also be the context for the block. Only available_context that is also in uses_context for the block should be part of the block's actual context. So I think the test failure in line 301 is expected and in fact correct, since you restrict the block type to only use "example", so "arbitrary" shouldn't be used.

That leads me to the other problem though that still exists: Since in WP_Block::render() the filtered $context is used in the inner block directly, it does not respect that block's uses_context property anymore.

Basically, it's not clear what render_block_context is exactly supposed to filter:

  • Is it supposed to filter the block's $context, explicitly allowing to ignore whatever the block's uses_context property says? That's what it's doing for inner blocks today.
    • If so, then discard my previous statement, and we need to fix this for how it works on the top-level block so that your test test_render_block_context_allowed_context() passes. For example, set the $context property on the block instance directly to the filtered value.
  • Is it actually supposed to filter the $available_context? On the top-level block, that's what it's doing today.
    • If so, we need to fix the refresh_context_dependents() method so that it sanitizes $context to still only contain what the block's uses_context allows for. $available_context should still include anything including arbitrary values.

@dlh01
Copy link

dlh01 commented Nov 12, 2024

Basically, it's not clear what render_block_context is exactly supposed to filter:

  • Is it supposed to filter the block's $context, explicitly allowing to ignore whatever the block's uses_context property says? That's what it's doing for inner blocks today.

    • If so, then discard my previous statement, and we need to fix this for how it works on the top-level block so that your test test_render_block_context_allowed_context() passes. For example, set the $context property on the block instance directly to the filtered value.
  • Is it actually supposed to filter the $available_context? On the top-level block, that's what it's doing today.

    • If so, we need to fix the refresh_context_dependents() method so that it sanitizes $context to still only contain what the block's uses_context allows for. $available_context should still include anything including arbitrary values.

@felixarntz I wanted to make sure you saw this older conversation in the other PR about this topic, where @gziolo mentioned that at least some of the time, the filter is meant to override uses_context: #7344 (comment). That conversation is what led to the arbitrary test that I switched to in 90c63a2.

@felixarntz
Copy link
Member

@dlh01 Thank you for providing this context.

In that case, I would argue we should adjust how this is handled in render_block(), so that the filter allows overriding uses_context as well, for consistency. For example, in addition to passing the provided data into the constructor ($available_context), we should right after that set the $context property to it on the instantiated block.

This way, it should behave consistently going forward, with the filter allowing to override $uses_context everywhere it's applied.

@gziolo
Copy link
Member

gziolo commented Nov 19, 2024

Existing usage of render_block_context in WordPress core:

function _block_template_render_without_post_block_context( $context ) {
/*
* When loading a template directly and not through a page that resolves it,
* the top-level post ID and type context get set to that of the template.
* Templates are just the structure of a site, and they should not be available
* as post context because blocks like Post Content would recurse infinitely.
*/
if ( isset( $context['postType'] ) && 'wp_template' === $context['postType'] ) {
unset( $context['postId'] );
unset( $context['postType'] );
}
return $context;
}

/**
* We set the `pattern/overrides` context through the `render_block_context`
* filter so that it is available when a pattern's inner blocks are
* rendering via do_blocks given it only receives the inner content.
*/
$has_pattern_overrides = isset( $attributes['content'] ) && null !== get_block_bindings_source( 'core/pattern-overrides' );
if ( $has_pattern_overrides ) {
$filter_block_context = static function ( $context ) use ( $attributes ) {
$context['pattern/overrides'] = $attributes['content'];
return $context;
};
add_filter( 'render_block_context', $filter_block_context, 1 );
}
$content = do_blocks( $content );

foreach ( $comments as $comment ) {
$comment_id = $comment->comment_ID;
$filter_block_context = static function ( $context ) use ( $comment_id ) {
$context['commentId'] = $comment_id;
return $context;
};
/*
* We set commentId context through the `render_block_context` filter so
* that dynamically inserted blocks (at `render_block` filter stage)
* will also receive that context.
*
* Use an early priority to so that other 'render_block_context' filters
* have access to the values.
*/
add_filter( 'render_block_context', $filter_block_context, 1 );
/*
* We construct a new WP_Block instance from the parsed block so that
* it'll receive any changes made by the `render_block_data` filter.
*/
$block_content = ( new WP_Block( $block->parsed_block ) )->render( array( 'dynamic' => false ) );

while ( $query->have_posts() ) {
$query->the_post();
// Get an instance of the current Post Template block.
$block_instance = $block->parsed_block;
// Set the block name to one that does not correspond to an existing registered block.
// This ensures that for the inner instances of the Post Template block, we do not render any block supports.
$block_instance['blockName'] = 'core/null';
$post_id = get_the_ID();
$post_type = get_post_type();
$filter_block_context = static function ( $context ) use ( $post_id, $post_type ) {
$context['postType'] = $post_type;
$context['postId'] = $post_id;
return $context;
};
// Use an early priority to so that other 'render_block_context' filters have access to the values.
add_filter( 'render_block_context', $filter_block_context, 1 );
// Render the inner blocks of the Post Template block with `dynamic` set to `false` to prevent calling
// `render_callback` and ensure that no wrapper markup is included.
$block_content = ( new WP_Block( $block_instance ) )->render( array( 'dynamic' => false ) );


My understanding of how it should work is as follows:

  • Every block can define providesContext with the list of attributes that are going to be exposed as block context.
  • render_block_context allows overriding the values set with providesContext or inject new properties when necessary.
  • uses_context needs to be always explicitly used to read the provided context no matter how it was set. If you inspect.

In practical terms, it means that available_context is properly documented:

/**
* All available context of the current hierarchy.
*
* @since 5.5.0
* @var array
* @access protected
*/
protected $available_context;

It's the property that gets impacted by the render_block_context filter. It also means that the block's context is derived from the available context:

https://github.com/WordPress/wordpress-develop/blob/dec5c906459c5cdebe7630fb6a61bcbf3e4344b9/src/wp-includes/class-wp-block.php#L44C2-L50C28

It gets filtered by scanning uses_context, which can be provided in the block definition, or through Block Bindings registration. Regardless, whenever $this->available_context changes, then $this->context should get updated, too.


Now diving into the code again, to see how it fits into the model based on my last assessment. Let's start with render_block utility in trunk.

$context = apply_filters( 'render_block_context', $context, $parsed_block, $parent_block );
$block = new WP_Block( $parsed_block, $context );
return $block->render();

In this case, it might be helpful to rename $context to $actual_context. However, so far it fits into the picture. Whatever gets filtered with render_block_context will be provided for consumption inside the block context for every block. It would also be easier to reason if this filter was renamed to render_block_available_context to better reflect how it impacts the rendering pipeline.

Let's move now to WP_Block::render in trunk. The logic applies only to child blocks:

$source_block = $inner_block->parsed_block;
/** This filter is documented in wp-includes/blocks.php */
$inner_block->parsed_block = apply_filters( 'render_block_data', $inner_block->parsed_block, $source_block, $parent_block );
/** This filter is documented in wp-includes/blocks.php */
$inner_block->context = apply_filters( 'render_block_context', $inner_block->context, $inner_block->parsed_block, $parent_block );
$block_content .= $inner_block->render();

This part doesn't fit the model I illustrated above. The changes from the filter get applied to the derived context of the child block. In practice, it means that any property can be exposed to this block even if it wasn't list inside uses_context or provided from any of the parent blocks. So, in this instance, render_block_context name fits very well to how it works.

@gziolo
Copy link
Member

gziolo commented Nov 19, 2024

In that case, I would argue we should adjust how this is handled in render_block(), so that the filter allows overriding uses_context as well, for consistency. For example, in addition to passing the provided data into the constructor ($available_context), we should right after that set the $context property to it on the instantiated block.

In #7522 (comment) I shared the way I was always thinking about block context and related filters. Now, let's go with the most realistic approach which is tied to the current implementation. In practice, the contract established with uses_context is violated by the code that allows extending the specific context providef for the individual block. I tend to agree that we should replicate that when using the render_block utility that as of today changes the available context, but doesn't force new properties to be exposed to block rendering engine unless they are defined with uses_context. I think we need to fix it in the first place for consistency. In effect, we probably in both places need to adjust both available_context and context so they both always stay in sync, plus the context for individual block contains properties allows through uses_context and these exposed by the filter.

@gziolo
Copy link
Member

gziolo commented Nov 19, 2024

For the sake of the experiment, I played a bit with changes that try to aggressively run the same hook to update both the available context and the context:

Index: src/wp-includes/blocks.php
===================================================================
--- src/wp-includes/blocks.php	(revision 59420)
+++ src/wp-includes/blocks.php	(working copy)
@@ -2163,6 +2163,8 @@
 		$context['postType'] = $post->post_type;
 	}
 
+	$block = new WP_Block( $parsed_block, $context );
+
 	/**
 	 * Filters the default context provided to a rendered block.
 	 *
@@ -2183,10 +2185,8 @@
 	 * }
 	 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block.
 	 */
-	$context = apply_filters( 'render_block_context', $context, $parsed_block, $parent_block );
+	$block->context = apply_filters( 'render_block_context', $block->context, $block->parsed_block, $parent_block );
 
-	$block = new WP_Block( $parsed_block, $context );
-
 	return $block->render();
 }
 
Index: src/wp-includes/class-wp-block.php
===================================================================
--- src/wp-includes/class-wp-block.php	(revision 59420)
+++ src/wp-includes/class-wp-block.php	(working copy)
@@ -138,7 +138,8 @@
 
 		$this->block_type = $registry->get_registered( $this->name );
 
-		$this->available_context = $available_context;
+		/** This filter is documented in wp-includes/blocks.php */
+		$this->available_context = apply_filters( 'render_block_context', $available_context, $this->parsed_block, null );
 
 		if ( ! empty( $this->block_type->uses_context ) ) {
 			foreach ( $this->block_type->uses_context as $context_name ) {

I passed null as the parent block in the constructor of WP Block, but it probably should be handled better.

@felixarntz
Copy link
Member

felixarntz commented Nov 19, 2024

@gziolo I think I understand most of your exploration, but not your conclusion of how it should behave.

Sharing my understanding and asking for clarification:

  • How I understand your comment render_block_context another approach that solve the context issue #7522 (comment) is that render_block_context should filter the block's $available_context. This means it indirectly also filters $context, but only for the values in the filtered $available_context that are allowlisted in $uses_context.
  • Based on this, what render_block() does is correct, and what WP_Block::render() does for its child blocks is incorrect (because it filters the $context itself directly, allowing to ignore $uses_context). So far, so good.
  • But then I don't understand what you're concluding in render_block_context another approach that solve the context issue #7522 (comment) and why. That seems to contradict what you said before. Can you clarify why the behavior should be different from what you outlined before as the ideal behavior?
  • That question also applies to render_block_context another approach that solve the context issue #7522 (comment), though here I would point out specifically that I don't think it's a good idea to use the same filter to filter $context in one place and $available_context in another place. That's what Core is already doing today, and it causes the problems. We need to make it behave consistently one way or another.
  • So far, what would make more sense to me based on your first comment is that WP_Block::render() is adjusted to filter the child block's $available_context, and based on that the child block's $context should be updated from it, but only for those keys included in $uses_context.

@gziolo
Copy link
Member

gziolo commented Nov 20, 2024

I think I understand most of your exploration, but not your conclusion of how it should behave.

I'm not surprised as it's a very complex problem to tackle as we have an important backward compatibility problem to solve. I offered my preferred solution in #7522 (comment) (that might require some deprecation, phase out for current behavior, detailed dev note), and an alternative (#7522 (comment)) that should work in most cases (the dev note might be sufficient). We should pick one.

So far, what would make more sense to me based on your first comment is that WP_Block::render() is adjusted to filter the child block's $available_context, and based on that the child block's $context should be updated from it, but only for those keys included in $uses_context.

Again, I'm open to the idea of applying the changes you proposed. I only wanted to emphasize again it's hard to predict what that would cause as a side effect. In some cases, inner block during rendering might depend on the presence of the context that isn't explicitly exposed through uses_context. Overall, I don't like this behavior introduced in the inner block rendering as this is something that doesn't translate well to the editor, where React offers a much more predictable interface for how you pair the provider and consumer when working with the context.

Anyone from @WordPress/gutenberg-core willing to chime in and share their opinions on this topic that will help to make the final decision?

@felixarntz
Copy link
Member

@gziolo Regarding backward compatibility, I assume you're referring to plugins that may potentially access a block's $context and expect to have one of the filtered pieces of data in there even if it is not in the block's $uses_context, based on the current behavior?

We could potentially still fix the behavior to get to the preferred state, but have a deprecation strategy to not break such behavior right away. Something like:

  • We change the inner block behavior to first apply the filtered value to $available_context. Then the $context will be "re-generated" based on the modified $available_context and the unchanged $uses_context. That's the ideal and eventual state.
  • We then also set the $context value directly to the filtered value, to (for now) maintain that behavior. But before doing that, we check whether there is a diff between the "correct" $context and the overriding $context and store that. We can use that to trigger a deprecation warning if any code accesses one of those context values (because eventually those shouldn't be present anymore).
  • To catch the problematic accesses, we could implement a simple class, e.g. WP_Block_Context which implements ArrayAccess and use that instead of an array for the $context parameter. By using a class, we are able to know which keys are accessed and can trigger the deprecation warning if it's one of the keys that should ideally not be allowed anymore.

WDYT?

@gziolo
Copy link
Member

gziolo commented Nov 20, 2024

Yes, that’s exactly what I was thinking about in terms of backward compatibility. It seems like a viable path forward. We can explore specific details of the depreciation strategy separately later. I was thinking also about making the context a virtual property and handle everything through magic methods. Whatever works best here 😀

@felixarntz
Copy link
Member

I was thinking also about making the context a virtual property and handle everything through magic methods.

That was my first thought too, but I think that wouldn't work because magic methods would only be called for the entire $context property, so we wouldn't know which keys someone is accessing on it. That's how I got to the ArrayAccess class idea.

@gziolo
Copy link
Member

gziolo commented Nov 20, 2024

I think got it know, we would pass WP_Block_Context instance to the filter when processing the inner block. This way we would be able to easily derive the context update, based on detected changes in the class. Interesting idea 👍🏻

@felixarntz
Copy link
Member

I think got it know, we would pass WP_Block_Context instance to the filter when processing the inner block. This way we would be able to easily derive the context update, based on detected changes in the class. Interesting idea 👍🏻

That's actually not entirely what I thought, but that's even better than what I thought 😆 So let's try that.

@joemcgill
Copy link
Member

@gziolo and @felixarntz, I've been spending time today catching up on this conversation and wanted to summarize what I understand to be the current thinking. Based on what @gziolo said is his preferred solution, the render_block_context filter should be filtering the available_context that a block can access, based on it's own uses_context properties and NOT be filtering the actual context property of a block before it is rendered.

Conceptually, this makes sense to me, and I think could be achieved by filtering the $child_context passed to WP_Block_List here.

In practice, however, the render_block_context filter seems to be used to affect the context property during rendering even in Core's own application of the filter in the comment template block and the post template block, so I do think planning a deprecation path as has been discussed would be important and would also likely require additional changes to the places where context is being dynamically filtered in certain blocks.

I've put together the following visual to help illustrate the concept of block context as it currently exists:

block context illustration

Restating what is in the illustration:

  1. Initially, the available context gets filtered before the root block is initialized (ref)
  2. The root block adds additional context to the available context prior to instantiating innerBlocks (ref)
  3. innerBlock context gets filtered during rendering, but available context is not affected by this filter (ref)

If applying the filter to available_context is the goal, I think we should start with fixing that bug, as is the expected behavior preferred by @dlh01 in the original ticket description:

Between the two options, I would expect the filter to behave like it does in render_block(): Context supplied with a filter is made available to inner blocks.

Whether or not we then change the current ability for a block's context property to be modified to include values that a block itself does not have listed in its 'uses_context' property can be a separate decision.

@felixarntz
Copy link
Member

Thanks for the helpful summary and visualization @joemcgill.

It makes sense for now to focus on only addressing the bug that $available_context of an inner block is currently not affected and thus not correctly passed to its child blocks.

As far as I can tell, to fix that this PR is almost there. It would be great to get your code review as well based on your understanding.

@gziolo
Copy link
Member

gziolo commented Dec 11, 2024

@joemcgill, thank you for your analysis to this complex problem. I agree with the proposed path forward, where the focus moves on solving one problem at a time. In effect, we seem to have a path forward to land this PR after addressing the feedback continued by @felixarntz. I'm happy to have another look from that angle next week.

@mukeshpanchal27
Copy link
Member Author

@felixarntz I have addressed the recent feedback and PR is ready for review.

I'm happy to have another look from that angle next week.

@gziolo Happy to get your feedback if you have moments. Thanks.

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

Successfully merging this pull request may close these issues.

6 participants