diff --git a/docs/designers-developers/developers/block-api/block-context.md b/docs/designers-developers/developers/block-api/block-context.md index 0b95807ffc24ee..4ee517d148e1af 100644 --- a/docs/designers-developers/developers/block-api/block-context.md +++ b/docs/designers-developers/developers/block-api/block-context.md @@ -63,4 +63,14 @@ registerBlockType( 'my-plugin/record-title', { ### PHP -_The PHP implementation of block context is currently experimental and subject to breaking changes. It will be documented in the future once the API has stabilized._ +A block's context values are available from the `context` property of the `$block` argument passed as the third argument to the `render_callback` function. + +`record-title/index.php` + +```php +register_block_type( 'my-plugin/record-title', array( + 'render_callback' => function( $attributes, $content, $block ) { + return 'The current record ID is: ' . $block->context['my-plugin/recordId']; + }, +) ); +``` diff --git a/lib/class-wp-block-list.php b/lib/class-wp-block-list.php new file mode 100644 index 00000000000000..281fa6ed4deb68 --- /dev/null +++ b/lib/class-wp-block-list.php @@ -0,0 +1,189 @@ +blocks = $blocks; + $this->available_context = $available_context; + $this->registry = $registry; + } + + /* + * ArrayAccess interface methods. + */ + + /** + * Returns true if a block exists by the specified block index, or false + * otherwise. + * + * @link https://www.php.net/manual/en/arrayaccess.offsetexists.php + * + * @param string $index Index of block to check. + * + * @return bool Whether block exists. + */ + public function offsetExists( $index ) { + return isset( $this->blocks[ $index ] ); + } + + /** + * Returns the value by the specified block index. + * + * @link https://www.php.net/manual/en/arrayaccess.offsetget.php + * + * @param string $index Index of block value to retrieve. + * + * @return mixed|null Block value if exists, or null. + */ + public function offsetGet( $index ) { + $block = $this->blocks[ $index ]; + + if ( isset( $block ) && is_array( $block ) ) { + $block = new WP_Block( $block, $this->available_context, $this->registry ); + $this->blocks[ $index ] = $block; + } + + return $block; + } + + /** + * Assign a block value by the specified block index. + * + * @link https://www.php.net/manual/en/arrayaccess.offsetset.php + * + * @param string $index Index of block value to set. + * @param mixed $value Block value. + */ + public function offsetSet( $index, $value ) { + if ( is_null( $index ) ) { + $this->blocks[] = $value; + } else { + $this->blocks[ $index ] = $value; + } + } + + /** + * Unset a block. + * + * @link https://www.php.net/manual/en/arrayaccess.offsetunset.php + * + * @param string $index Index of block value to unset. + */ + public function offsetUnset( $index ) { + unset( $this->blocks[ $index ] ); + } + + /* + * Iterator interface methods. + */ + + /** + * Rewinds back to the first element of the Iterator. + * + * @link https://www.php.net/manual/en/iterator.rewind.php + */ + public function rewind() { + reset( $this->blocks ); + } + + /** + * Returns the current element of the block list. + * + * @link https://www.php.net/manual/en/iterator.current.php + * + * @return mixed Current element. + */ + public function current() { + return $this->offsetGet( $this->key() ); + } + + /** + * Returns the key of the current element of the block list. + * + * @link https://www.php.net/manual/en/iterator.key.php + * + * @return mixed Key of the current element. + */ + public function key() { + return key( $this->blocks ); + } + + /** + * Moves the current position of the block list to the next element. + * + * @link https://www.php.net/manual/en/iterator.next.php + */ + public function next() { + next( $this->blocks ); + } + + /** + * Checks if current position is valid. + * + * @link https://www.php.net/manual/en/iterator.valid.php + */ + public function valid() { + return null !== key( $this->blocks ); + } + + /* + * Countable interface methods. + */ + + /** + * Returns the count of blocks in the list. + * + * @link https://www.php.net/manual/en/countable.count.php + * + * @return int Block count. + */ + public function count() { + return count( $this->blocks ); + } + +} diff --git a/lib/class-wp-block.php b/lib/class-wp-block.php index 55bcb9ecc5d4fb..d362dea65a0713 100644 --- a/lib/class-wp-block.php +++ b/lib/class-wp-block.php @@ -10,6 +10,13 @@ */ class WP_Block { + /** + * Original parsed array representation of block. + * + * @var array + */ + public $parsed_block; + /** * Name of block. * @@ -41,13 +48,6 @@ class WP_Block { */ protected $available_context; - /** - * Block attribute values. - * - * @var array - */ - public $attributes = array(); - /** * List of inner blocks (of this same class) * @@ -94,7 +94,8 @@ class WP_Block { * @param WP_Block_Type_Registry $registry Optional block type registry. */ public function __construct( $block, $available_context = array(), $registry = null ) { - $this->name = $block['blockName']; + $this->parsed_block = $block; + $this->name = $block['blockName']; if ( is_null( $registry ) ) { $registry = WP_Block_Type_Registry::get_instance(); @@ -102,14 +103,6 @@ public function __construct( $block, $available_context = array(), $registry = n $this->block_type = $registry->get_registered( $this->name ); - if ( ! empty( $block['attrs'] ) ) { - $this->attributes = $block['attrs']; - } - - if ( ! is_null( $this->block_type ) ) { - $this->attributes = $this->block_type->prepare_attributes_for_render( $this->attributes ); - } - $this->available_context = $available_context; if ( ! empty( $this->block_type->context ) ) { @@ -133,12 +126,7 @@ public function __construct( $block, $available_context = array(), $registry = n } /* phpcs:enable */ - $this->inner_blocks = array_map( - function( $inner_block ) use ( $child_context, $registry ) { - return new WP_Block( $inner_block, $child_context, $registry ); - }, - $block['innerBlocks'] - ); + $this->inner_blocks = new WP_Block_List( $block['innerBlocks'], $child_context, $registry ); } if ( ! empty( $block['innerHTML'] ) ) { @@ -150,13 +138,41 @@ function( $inner_block ) use ( $child_context, $registry ) { } } + /** + * Returns a value from an inaccessible property. + * + * This is used to lazily initialize the `attributes` property of a block, + * such that it is only prepared with default attributes at the time that + * the property is accessed. For all other inaccessible properties, a `null` + * value is returned. + * + * @param string $name Property name. + * + * @return array|null Prepared attributes, or null. + */ + public function __get( $name ) { + if ( 'attributes' === $name ) { + $this->attributes = isset( $this->parsed_block['attrs'] ) ? + $this->parsed_block['attrs'] : + array(); + + if ( ! is_null( $this->block_type ) ) { + $this->attributes = $this->block_type->prepare_attributes_for_render( $this->attributes ); + } + + return $this->attributes; + } + + return null; + } + /** * Generates the render output for the block. * * @return string Rendered block output. */ public function render() { - global $post, $_experimental_block; + global $post; $is_dynamic = $this->name && null !== $this->block_type && $this->block_type->is_dynamic(); $block_content = ''; @@ -169,15 +185,13 @@ public function render() { } if ( $is_dynamic ) { - $global_post = $post; - $global_block = $_experimental_block; - $_experimental_block = $this; - $block_content = (string) call_user_func( $this->block_type->render_callback, $this->attributes, $block_content ); - $_experimental_block = $global_block; - $post = $global_post; + $global_post = $post; + $block_content = (string) call_user_func( $this->block_type->render_callback, $this->attributes, $block_content, $this ); + $post = $global_post; } - return $block_content; + /** This filter is documented in src/wp-includes/blocks.php */ + return apply_filters( 'render_block', $block_content, $this->parsed_block ); } } diff --git a/lib/compat.php b/lib/compat.php index e38f339030cb5b..48f3c7db728cf7 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -175,3 +175,48 @@ function gutenberg_get_post_from_context() { } return get_post(); } + +/** + * Shim that hooks into `pre_render_block` so as to override `render_block` with + * a function that assigns block context. + * + * This can be removed when plugin support requires WordPress 5.5.0+. + * + * @see https://core.trac.wordpress.org/ticket/49927 + * + * @param string|null $pre_render The pre-rendered content. Defaults to null. + * @param array $parsed_block The parsed block being rendered. + * + * @return string String of rendered HTML. + */ +function gutenberg_render_block_with_assigned_block_context( $pre_render, $parsed_block ) { + global $post; + + /* + * If a non-null value is provided, a filter has run at an earlier priority + * and has already handled custom rendering and should take precedence. + */ + if ( null !== $pre_render ) { + return $pre_render; + } + + $source_block = $parsed_block; + + /** This filter is documented in src/wp-includes/blocks.php */ + $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block ); + $context = array( + 'postId' => $post->ID, + + /* + * The `postType` context is largely unnecessary server-side, since the + * ID is usually sufficient on its own. That being said, since a block's + * manifest is expected to be shared between the server and the client, + * it should be included to consistently fulfill the expectation. + */ + 'postType' => $post->post_type, + ); + $block = new WP_Block( $parsed_block, $context ); + + return $block->render(); +} +add_filter( 'pre_render_block', 'gutenberg_render_block_with_assigned_block_context', 9, 2 ); diff --git a/lib/load.php b/lib/load.php index 1dca8b64972c56..ce774043902877 100644 --- a/lib/load.php +++ b/lib/load.php @@ -67,6 +67,10 @@ function gutenberg_is_experiment_enabled( $name ) { require dirname( __FILE__ ) . '/class-wp-block.php'; } +if ( ! class_exists( 'WP_Block_List' ) ) { + require dirname( __FILE__ ) . '/class-wp-block-list.php'; +} + require dirname( __FILE__ ) . '/compat.php'; require dirname( __FILE__ ) . '/blocks.php'; diff --git a/packages/block-library/src/post-title/index.php b/packages/block-library/src/post-title/index.php index d6218034553f32..0e30b478a39fe2 100644 --- a/packages/block-library/src/post-title/index.php +++ b/packages/block-library/src/post-title/index.php @@ -8,15 +8,18 @@ /** * Renders the `core/post-title` block on the server. * + * @param array $attributes Block attributes. + * @param string $content Block default content. + * @param WP_Block $block Block instance. + * * @return string Returns the filtered post title for the current post wrapped inside "h1" tags. */ -function render_block_core_post_title() { - $post = gutenberg_get_post_from_context(); - if ( ! $post ) { +function render_block_core_post_title( $attributes, $content, $block ) { + if ( ! isset( $block->context['postId'] ) ) { return ''; } - return '