From d049341fb3950176e734b27ab7907bb6327e29b4 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 27 Apr 2020 09:53:49 -0400 Subject: [PATCH 01/18] Block API: WP_Block: Run filter on rendered content --- lib/class-wp-block.php | 13 +++++++++++-- lib/compat.php | 33 +++++++++++++++++++++++++++++++++ phpunit/class-wp-block-test.php | 1 + 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-block.php b/lib/class-wp-block.php index 55bcb9ecc5d4f..c4a83c055c4ad 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. * @@ -94,7 +101,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(); @@ -177,7 +185,8 @@ public function render() { $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 e38f339030cb5..f4591b33c03a7 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -175,3 +175,36 @@ 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 ); + $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/phpunit/class-wp-block-test.php b/phpunit/class-wp-block-test.php index dba70ae06c09a..4349994d62ab4 100644 --- a/phpunit/class-wp-block-test.php +++ b/phpunit/class-wp-block-test.php @@ -40,6 +40,7 @@ function test_constructor_assigns_properties_from_parsed_block() { $context = array(); $block = new WP_Block( $parsed_block, $context, $this->registry ); + $this->assertSame( $parsed_block, $block->parsed_block ); $this->assertEquals( $parsed_block['blockName'], $block->name ); $this->assertEquals( $parsed_block['attrs'], $block->attributes ); $this->assertEquals( $parsed_block['innerContent'], $block->inner_content ); From 4bde09c68f6c4b30658242bc929027fc0a12714f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 27 Apr 2020 09:55:21 -0400 Subject: [PATCH 02/18] Block API: WP_Block: Prepare attributes for render at render --- lib/class-wp-block.php | 11 ++-- phpunit/class-wp-block-test.php | 107 +++++++++++--------------------- 2 files changed, 43 insertions(+), 75 deletions(-) diff --git a/lib/class-wp-block.php b/lib/class-wp-block.php index c4a83c055c4ad..c4a17406820ad 100644 --- a/lib/class-wp-block.php +++ b/lib/class-wp-block.php @@ -114,10 +114,6 @@ public function __construct( $block, $available_context = array(), $registry = n $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 ) ) { @@ -177,10 +173,15 @@ public function render() { } if ( $is_dynamic ) { + $attributes = $this->attributes; + if ( ! is_null( $this->block_type ) ) { + $attributes = $this->block_type->prepare_attributes_for_render( $attributes ); + } + $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 ); + $block_content = (string) call_user_func( $this->block_type->render_callback, $attributes, $block_content ); $_experimental_block = $global_block; $post = $global_post; } diff --git a/phpunit/class-wp-block-test.php b/phpunit/class-wp-block-test.php index 4349994d62ab4..ba6385b211aee 100644 --- a/phpunit/class-wp-block-test.php +++ b/phpunit/class-wp-block-test.php @@ -69,60 +69,6 @@ function test_constructor_assigns_block_type_from_registry() { ); } - function test_constructor_assigns_attributes_with_defaults() { - $this->registry->register( - 'core/example', - array( - 'attributes' => array( - 'defaulted' => array( - 'type' => 'number', - 'default' => 10, - ), - ), - ) - ); - - $parsed_block = array( - 'blockName' => 'core/example', - 'attrs' => array( - 'explicit' => 20, - ), - ); - $context = array(); - $block = new WP_Block( $parsed_block, $context, $this->registry ); - - $this->assertEquals( - array( - 'defaulted' => 10, - 'explicit' => 20, - ), - $block->attributes - ); - } - - function test_constructor_assigns_attributes_with_only_defaults() { - $this->registry->register( - 'core/example', - array( - 'attributes' => array( - 'defaulted' => array( - 'type' => 'number', - 'default' => 10, - ), - ), - ) - ); - - $parsed_block = array( - 'blockName' => 'core/example', - 'attrs' => array(), - ); - $context = array(); - $block = new WP_Block( $parsed_block, $context, $this->registry ); - - $this->assertEquals( array( 'defaulted' => 10 ), $block->attributes ); - } - function test_constructor_assigns_context_from_block_type() { $this->registry->register( 'core/example', @@ -228,6 +174,40 @@ function test_constructor_assigns_merged_context() { ); } + function test_render_assigns_attributes_with_defaults() { + $this->registry->register( + 'core/example', + array( + 'attributes' => array( + 'defaulted' => array( + 'type' => 'number', + 'default' => 10, + ), + ), + 'render_callback' => function( $attributes ) { + return json_encode( + array( + 'explicit' => $attributes['explicit'], + 'defaulted' => $attributes['defaulted'], + ) + ); + } + ) + ); + + $parsed_block = array( + 'blockName' => 'core/example', + 'attrs' => array( + 'explicit' => 20, + ), + ); + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + $content = $block->render(); + + $this->assertEquals( '{"explicit":20,"defaulted":10}', $content ); + } + function test_render_static_block_type_returns_own_content() { $this->registry->register( 'core/static', array() ); $this->registry->register( @@ -251,33 +231,20 @@ function test_render_assigns_instance_global_for_render_callback() { $this->registry->register( 'core/greeting', array( - 'attributes' => array( - 'toWhom' => array( - 'type' => 'string', - ), - 'punctuation' => array( - 'type' => 'string', - 'default' => '!', - ), - ), 'render_callback' => function() { global $_experimental_block; - return sprintf( - 'Hello %s%s', - $_experimental_block->attributes['toWhom'], - $_experimental_block->attributes['punctuation'] - ); + return sprintf( 'Hello from %s', $_experimental_block->name ); }, ) ); - $parsed_blocks = parse_blocks( '' ); + $parsed_blocks = parse_blocks( '' ); $parsed_block = $parsed_blocks[0]; $context = array(); $block = new WP_Block( $parsed_block, $context, $this->registry ); - $this->assertSame( 'Hello world!', $block->render() ); + $this->assertSame( 'Hello from core/greeting', $block->render() ); } function test_passes_attributes_to_render_callback() { From 52db02b5df250360ba45e7ca07a12d5298c6fed7 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 27 Apr 2020 10:08:27 -0400 Subject: [PATCH 03/18] Block API: WP_Block: Pass block as third argument of render_callback --- lib/class-wp-block.php | 11 ++++------- packages/block-library/src/post-title/index.php | 7 +++---- packages/e2e-tests/plugins/block-context.php | 11 ++++++++++- phpunit/class-block-context-test.php | 6 ++---- phpunit/class-wp-block-test.php | 8 +++----- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/class-wp-block.php b/lib/class-wp-block.php index c4a17406820ad..9c410cef7ca88 100644 --- a/lib/class-wp-block.php +++ b/lib/class-wp-block.php @@ -160,7 +160,7 @@ function( $inner_block ) use ( $child_context, $registry ) { * @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 = ''; @@ -178,12 +178,9 @@ public function render() { $attributes = $this->block_type->prepare_attributes_for_render( $attributes ); } - $global_post = $post; - $global_block = $_experimental_block; - $_experimental_block = $this; - $block_content = (string) call_user_func( $this->block_type->render_callback, $attributes, $block_content ); - $_experimental_block = $global_block; - $post = $global_post; + $global_post = $post; + $block_content = (string) call_user_func( $this->block_type->render_callback, $attributes, $block_content, $this ); + $post = $global_post; } /** This filter is documented in src/wp-includes/blocks.php */ diff --git a/packages/block-library/src/post-title/index.php b/packages/block-library/src/post-title/index.php index d6218034553f3..d07a02fa8a4d2 100644 --- a/packages/block-library/src/post-title/index.php +++ b/packages/block-library/src/post-title/index.php @@ -10,13 +10,12 @@ * * @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 '

' . get_the_title( $post ) . '

'; + return '

' . get_the_title( $block->context['postId'] ) . '

'; } /** diff --git a/packages/e2e-tests/plugins/block-context.php b/packages/e2e-tests/plugins/block-context.php index 380660f6f08ef..8f3ca467d5004 100644 --- a/packages/e2e-tests/plugins/block-context.php +++ b/packages/e2e-tests/plugins/block-context.php @@ -47,7 +47,16 @@ function gutenberg_test_register_context_blocks() { register_block_type( 'gutenberg/test-context-consumer', array( - 'context' => array( 'gutenberg/recordId' ), + 'context' => array( 'gutenberg/recordId' ), + 'render_callback' => function( $attributes, $content, $block ) { + $record_id = $block->context['gutenberg/recordId']; + + if ( ! is_int( $record_id ) ) { + throw new Exception( 'Expected numeric recordId' ); + } + + return 'The record ID is: ' . filter_var( $record_id, FILTER_VALIDATE_INT ); + }, ) ); } diff --git a/phpunit/class-block-context-test.php b/phpunit/class-block-context-test.php index 769b0e0242916..5bc62d4c167e0 100644 --- a/phpunit/class-block-context-test.php +++ b/phpunit/class-block-context-test.php @@ -106,10 +106,8 @@ function test_provides_block_context() { 'gutenberg/contextWithAssigned', 'gutenberg/contextWithoutDefault', ), - 'render_callback' => function() use ( &$provided_context ) { - global $_experimental_block; - - $provided_context[] = $_experimental_block->context; + 'render_callback' => function( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context[] = $block->context; return ''; }, diff --git a/phpunit/class-wp-block-test.php b/phpunit/class-wp-block-test.php index ba6385b211aee..414d52a91bf6c 100644 --- a/phpunit/class-wp-block-test.php +++ b/phpunit/class-wp-block-test.php @@ -227,14 +227,12 @@ function test_render_static_block_type_returns_own_content() { $this->assertSame( 'abc', $block->render() ); } - function test_render_assigns_instance_global_for_render_callback() { + function test_render_passes_block_for_render_callback() { $this->registry->register( 'core/greeting', array( - 'render_callback' => function() { - global $_experimental_block; - - return sprintf( 'Hello from %s', $_experimental_block->name ); + 'render_callback' => function( $attributes, $content, $block ) { + return sprintf( 'Hello from %s', $block->name ); }, ) ); From 688d4b7f1aa65c8f139dd6731c6dfbd424cd19d6 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 29 Apr 2020 15:58:46 -0400 Subject: [PATCH 04/18] Block API: WP_Block: Use magic method for lazy attributes initialization --- lib/class-wp-block.php | 46 ++++++++++------- phpunit/class-wp-block-test.php | 88 ++++++++++++++++++++------------- 2 files changed, 83 insertions(+), 51 deletions(-) diff --git a/lib/class-wp-block.php b/lib/class-wp-block.php index 9c410cef7ca88..129c35db74fa9 100644 --- a/lib/class-wp-block.php +++ b/lib/class-wp-block.php @@ -48,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) * @@ -110,10 +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']; - } - $this->available_context = $available_context; if ( ! empty( $this->block_type->context ) ) { @@ -154,6 +143,34 @@ 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 && ! isset( $this->attributes ) ) { + $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. * @@ -173,13 +190,8 @@ public function render() { } if ( $is_dynamic ) { - $attributes = $this->attributes; - if ( ! is_null( $this->block_type ) ) { - $attributes = $this->block_type->prepare_attributes_for_render( $attributes ); - } - $global_post = $post; - $block_content = (string) call_user_func( $this->block_type->render_callback, $attributes, $block_content, $this ); + $block_content = (string) call_user_func( $this->block_type->render_callback, $this->attributes, $block_content, $this ); $post = $global_post; } diff --git a/phpunit/class-wp-block-test.php b/phpunit/class-wp-block-test.php index 414d52a91bf6c..7355d9b80a702 100644 --- a/phpunit/class-wp-block-test.php +++ b/phpunit/class-wp-block-test.php @@ -69,6 +69,60 @@ function test_constructor_assigns_block_type_from_registry() { ); } + function test_lazily_assigns_attributes_with_defaults() { + $this->registry->register( + 'core/example', + array( + 'attributes' => array( + 'defaulted' => array( + 'type' => 'number', + 'default' => 10, + ), + ), + ) + ); + + $parsed_block = array( + 'blockName' => 'core/example', + 'attrs' => array( + 'explicit' => 20, + ), + ); + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertEquals( + array( + 'defaulted' => 10, + 'explicit' => 20, + ), + $block->attributes + ); + } + + function test_lazily_assigns_attributes_with_only_defaults() { + $this->registry->register( + 'core/example', + array( + 'attributes' => array( + 'defaulted' => array( + 'type' => 'number', + 'default' => 10, + ), + ), + ) + ); + + $parsed_block = array( + 'blockName' => 'core/example', + 'attrs' => array(), + ); + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $this->assertEquals( array( 'defaulted' => 10 ), $block->attributes ); + } + function test_constructor_assigns_context_from_block_type() { $this->registry->register( 'core/example', @@ -174,40 +228,6 @@ function test_constructor_assigns_merged_context() { ); } - function test_render_assigns_attributes_with_defaults() { - $this->registry->register( - 'core/example', - array( - 'attributes' => array( - 'defaulted' => array( - 'type' => 'number', - 'default' => 10, - ), - ), - 'render_callback' => function( $attributes ) { - return json_encode( - array( - 'explicit' => $attributes['explicit'], - 'defaulted' => $attributes['defaulted'], - ) - ); - } - ) - ); - - $parsed_block = array( - 'blockName' => 'core/example', - 'attrs' => array( - 'explicit' => 20, - ), - ); - $context = array(); - $block = new WP_Block( $parsed_block, $context, $this->registry ); - $content = $block->render(); - - $this->assertEquals( '{"explicit":20,"defaulted":10}', $content ); - } - function test_render_static_block_type_returns_own_content() { $this->registry->register( 'core/static', array() ); $this->registry->register( From 9f24221eaf479d4617a356888f5c550548902313 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 29 Apr 2020 16:16:19 -0400 Subject: [PATCH 05/18] Block API: Implement iterable WP_Block_List --- lib/class-wp-block-list.php | 171 ++++++++++++++++++++++++++++++++++++ lib/class-wp-block.php | 7 +- lib/load.php | 4 + 3 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 lib/class-wp-block-list.php diff --git a/lib/class-wp-block-list.php b/lib/class-wp-block-list.php new file mode 100644 index 0000000000000..a1e0a766842c1 --- /dev/null +++ b/lib/class-wp-block-list.php @@ -0,0 +1,171 @@ +blocks = $blocks; + $this->available_context = $available_context; + $this->registry = is_null( $registry ) ? + WP_Block_Type_Registry::get_instance() : + $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 ); + } + + 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 ); + } + +} diff --git a/lib/class-wp-block.php b/lib/class-wp-block.php index 129c35db74fa9..085079942178a 100644 --- a/lib/class-wp-block.php +++ b/lib/class-wp-block.php @@ -126,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'] ) ) { diff --git a/lib/load.php b/lib/load.php index 1dca8b64972c5..ce77404390287 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'; From c205883e6c25468efc472721c27c357d00f59c0b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 29 Apr 2020 16:18:12 -0400 Subject: [PATCH 06/18] Block API: Unskip block context test --- phpunit/class-block-context-test.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/phpunit/class-block-context-test.php b/phpunit/class-block-context-test.php index 5bc62d4c167e0..b78911cf529c4 100644 --- a/phpunit/class-block-context-test.php +++ b/phpunit/class-block-context-test.php @@ -67,8 +67,6 @@ protected function register_block_type( $name, $args ) { * its inner blocks. */ function test_provides_block_context() { - $this->markTestSkipped(); - $provided_context = array(); $this->register_block_type( From d5298c0ced81ddd0a05768af91fbaa60c36c8284 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 29 Apr 2020 16:18:23 -0400 Subject: [PATCH 07/18] Block Library: Post Title: Document function attributes --- packages/block-library/src/post-title/index.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block-library/src/post-title/index.php b/packages/block-library/src/post-title/index.php index d07a02fa8a4d2..0e30b478a39fe 100644 --- a/packages/block-library/src/post-title/index.php +++ b/packages/block-library/src/post-title/index.php @@ -8,6 +8,10 @@ /** * 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( $attributes, $content, $block ) { From 87b20dc0494e163d28422d77acf5acce344a88b0 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Apr 2020 16:13:12 -0400 Subject: [PATCH 08/18] Block API: WP_Block_List: Format constructor to prepare defaults --- lib/class-wp-block-list.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-block-list.php b/lib/class-wp-block-list.php index a1e0a766842c1..607e4196c3b35 100644 --- a/lib/class-wp-block-list.php +++ b/lib/class-wp-block-list.php @@ -44,11 +44,13 @@ class WP_Block_List implements Iterator, ArrayAccess { * @param WP_Block_Type_Registry $registry Optional block type registry. */ public function __construct( $blocks, $available_context = array(), $registry = null ) { + if ( is_null( $registry ) ) { + $registry = WP_Block_Type_Registry::get_instance(); + } + $this->blocks = $blocks; $this->available_context = $available_context; - $this->registry = is_null( $registry ) ? - WP_Block_Type_Registry::get_instance() : - $registry; + $this->registry = $registry; } /* From f0bbd32b7c503bd0982815b0024a6bedbde5780b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Apr 2020 16:13:36 -0400 Subject: [PATCH 09/18] Block API: WP_Block_List: Replace reference of initialized block --- lib/class-wp-block-list.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-block-list.php b/lib/class-wp-block-list.php index 607e4196c3b35..6592bfc6dbb46 100644 --- a/lib/class-wp-block-list.php +++ b/lib/class-wp-block-list.php @@ -84,7 +84,8 @@ public function offsetGet( $index ) { $block = $this->blocks[ $index ]; if ( isset( $block ) && is_array( $block ) ) { - $block = new WP_Block( $block, $this->available_context, $this->registry ); + $block = new WP_Block( $block, $this->available_context, $this->registry ); + $this->blocks[ $index ] = $block; } return $block; From 1c32a1cb30763bea7f069699f2c282b8a61fcc7d Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Apr 2020 16:15:48 -0400 Subject: [PATCH 10/18] Block API: WP_Block_List: Implement Countable --- lib/class-wp-block-list.php | 17 ++++++++++- phpunit/class-wp-block-list-test.php | 44 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 phpunit/class-wp-block-list-test.php diff --git a/lib/class-wp-block-list.php b/lib/class-wp-block-list.php index 6592bfc6dbb46..281fa6ed4deb6 100644 --- a/lib/class-wp-block-list.php +++ b/lib/class-wp-block-list.php @@ -8,7 +8,7 @@ /** * Class representing a list of block instances. */ -class WP_Block_List implements Iterator, ArrayAccess { +class WP_Block_List implements Iterator, ArrayAccess, Countable { /** * Original array of parsed block data. @@ -171,4 +171,19 @@ 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/phpunit/class-wp-block-list-test.php b/phpunit/class-wp-block-list-test.php new file mode 100644 index 0000000000000..8d81d3fb0774a --- /dev/null +++ b/phpunit/class-wp-block-list-test.php @@ -0,0 +1,44 @@ +registry = new WP_Block_Type_Registry(); + $this->registry->register( 'core/example', array() ); + } + + /** + * Tear down each test method. + */ + public function tearDown() { + parent::tearDown(); + + $this->registry = null; + } + + function test_countable() { + $parsed_blocks = parse_blocks( '' ); + $context = array(); + $blocks = new WP_Block_List( $parsed_blocks, $context, $this->registry ); + + $this->assertEquals( 1, count( $blocks ) ); + } + +} From 494e62ed70b2e017284318c9073e5027ca9f80ba Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Apr 2020 16:53:12 -0400 Subject: [PATCH 11/18] Block API: WP_Block_List: Add unit tests for array access, iterable --- phpunit/class-wp-block-list-test.php | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/phpunit/class-wp-block-list-test.php b/phpunit/class-wp-block-list-test.php index 8d81d3fb0774a..ed6565f7a9f94 100644 --- a/phpunit/class-wp-block-list-test.php +++ b/phpunit/class-wp-block-list-test.php @@ -33,6 +33,56 @@ public function tearDown() { $this->registry = null; } + function test_array_access() { + $parsed_blocks = parse_blocks( '' ); + $context = array(); + $blocks = new WP_Block_List( $parsed_blocks, $context, $this->registry ); + + // Test "offsetExists". + $this->assertTrue( isset( $blocks[0] ) ); + + // Test "offsetGet". + $this->assertEquals( 'core/example', $blocks[0]->name ); + + // Test "offsetSet". + $parsed_blocks[0]['blockName'] = 'core/updated'; + $blocks[0] = new WP_Block( $parsed_blocks[0], $context, $this->registry ); + $this->assertEquals( 'core/updated', $blocks[0]->name ); + + // Test "offsetUnset". + unset( $blocks[0] ); + $this->assertFalse( isset( $blocks[0] ) ); + } + + function test_iterable() { + $parsed_blocks = parse_blocks( '' ); + $context = array(); + $blocks = new WP_Block_List( $parsed_blocks, $context, $this->registry ); + $assertions = 0; + + foreach ( $blocks as $block ) { + $this->assertEquals( 'core/example', $block->name ); + $assertions++; + foreach ( $block->inner_blocks as $inner_block ) { + $this->assertEquals( 'core/example', $inner_block->name ); + $assertions++; + } + } + + $blocks->rewind(); + while ( $blocks->valid() ) { + $key = $blocks->key(); + $block = $blocks->current(); + $this->assertEquals( 0, $key ); + $assertions++; + $this->assertEquals( 'core/example', $block->name ); + $assertions++; + $blocks->next(); + } + + $this->assertEquals( 4, $assertions ); + } + function test_countable() { $parsed_blocks = parse_blocks( '' ); $context = array(); From c03051dfa560ba6f7cd082432a33b5ec08db94d3 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Apr 2020 17:06:27 -0400 Subject: [PATCH 12/18] Block API: WP_Block: Add test case for render_block filtering Include inner blocks as a noted previous regression --- phpunit/class-wp-block-test.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/phpunit/class-wp-block-test.php b/phpunit/class-wp-block-test.php index 7355d9b80a702..e4cec8ef21e1d 100644 --- a/phpunit/class-wp-block-test.php +++ b/phpunit/class-wp-block-test.php @@ -32,6 +32,10 @@ public function tearDown() { $this->registry = null; } + function filter_render_block( $content, $parsed_block ) { + return 'Original: "' . $content . '", from block "' . $parsed_block['blockName'] . '"'; + } + function test_constructor_assigns_properties_from_parsed_block() { $this->registry->register( 'core/example', array() ); @@ -265,6 +269,24 @@ function test_render_passes_block_for_render_callback() { $this->assertSame( 'Hello from core/greeting', $block->render() ); } + function test_render_applies_render_block_filter() { + $this->registry->register( 'core/example', array() ); + + add_filter( 'render_block', array( $this, 'filter_render_block' ), 10, 2 ); + + $parsed_blocks = parse_blocks( 'StaticInner' ); + $parsed_block = $parsed_blocks[0]; + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + + $rendered_content = $block->render(); + + remove_filter( 'render_block', array( $this, 'filter_render_block' ) ); + + $this->assertSame( 'Original: "StaticOriginal: "Inner", from block "core/example"", from block "core/example"', $rendered_content ); + + } + function test_passes_attributes_to_render_callback() { $this->registry->register( 'core/greeting', From a1492b8fcf54580d4b858a20e6c4593074f3f29d Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 1 May 2020 16:25:42 -0400 Subject: [PATCH 13/18] Docs: Restore documentation for block context PHP API --- .../developers/block-api/block-context.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/designers-developers/developers/block-api/block-context.md b/docs/designers-developers/developers/block-api/block-context.md index 0b95807ffc24e..4ee517d148e1a 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']; + }, +) ); +``` From c3e0b9d9a53033b858ad2d0eef1e73aa2993a0bd Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 4 May 2020 17:27:27 -0400 Subject: [PATCH 14/18] Block API: WP_Block: Remove unnecessary isset --- lib/class-wp-block.php | 2 +- phpunit/class-wp-block-test.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-block.php b/lib/class-wp-block.php index 085079942178a..d362dea65a071 100644 --- a/lib/class-wp-block.php +++ b/lib/class-wp-block.php @@ -151,7 +151,7 @@ public function __construct( $block, $available_context = array(), $registry = n * @return array|null Prepared attributes, or null. */ public function __get( $name ) { - if ( 'attributes' === $name && ! isset( $this->attributes ) ) { + if ( 'attributes' === $name ) { $this->attributes = isset( $this->parsed_block['attrs'] ) ? $this->parsed_block['attrs'] : array(); diff --git a/phpunit/class-wp-block-test.php b/phpunit/class-wp-block-test.php index e4cec8ef21e1d..236ed46c0cb0e 100644 --- a/phpunit/class-wp-block-test.php +++ b/phpunit/class-wp-block-test.php @@ -125,6 +125,8 @@ function test_lazily_assigns_attributes_with_only_defaults() { $block = new WP_Block( $parsed_block, $context, $this->registry ); $this->assertEquals( array( 'defaulted' => 10 ), $block->attributes ); + // Intentionally call a second time, to ensure property was assigned. + $this->assertEquals( array( 'defaulted' => 10 ), $block->attributes ); } function test_constructor_assigns_context_from_block_type() { From 6e6efd441e0086995d7c7f5a4f8518c999a5bd07 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 4 May 2020 17:28:26 -0400 Subject: [PATCH 15/18] Framework: Fix formatting for multi-line comment --- lib/compat.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/compat.php b/lib/compat.php index f4591b33c03a7..949e9bcc214b7 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -192,8 +192,10 @@ function gutenberg_get_post_from_context() { 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 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; } From cd3d81b07464a8177de112da760ec81db45ed519 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 4 May 2020 17:34:37 -0400 Subject: [PATCH 16/18] Framework: Include postType context server-side --- lib/compat.php | 11 ++++++++- phpunit/class-block-context-test.php | 34 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/compat.php b/lib/compat.php index 949e9bcc214b7..7e1487fc39a76 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -204,7 +204,16 @@ function gutenberg_render_block_with_assigned_block_context( $pre_render, $parse /** 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 ); + $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 + * manifests 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(); diff --git a/phpunit/class-block-context-test.php b/phpunit/class-block-context-test.php index b78911cf529c4..b69d4c4d74155 100644 --- a/phpunit/class-block-context-test.php +++ b/phpunit/class-block-context-test.php @@ -129,4 +129,38 @@ function test_provides_block_context() { ); } + /** + * Tests that a block can receive default-provided context through + * render_block. + */ + function test_provides_default_context() { + global $post; + + $provided_context = array(); + + $this->register_block_type( + 'gutenberg/test-context-consumer', + array( + 'context' => array( 'postId', 'postType' ), + 'render_callback' => function( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context[] = $block->context; + + return ''; + }, + ) + ); + + $parsed_blocks = parse_blocks( '' ); + + render_block( $parsed_blocks[0] ); + + $this->assertEquals( + array( + 'postId' => $post->ID, + 'postType' => $post->post_type, + ), + $provided_context[0] + ); + } + } From 3020f1b18d4a66dc29d2535299a02882bdf35c69 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 4 May 2020 17:45:12 -0400 Subject: [PATCH 17/18] Framework: Improve postType comment grammar Co-authored-by: Enrique Piqueras --- lib/compat.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compat.php b/lib/compat.php index 7e1487fc39a76..6c84309b017f3 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -208,8 +208,8 @@ function gutenberg_render_block_with_assigned_block_context( $pre_render, $parse '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 - * manifests is expected to be shared between the server and the client, + * 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, From 75f64c96b82e24ff711be1ec0d8ee03cfef423bb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 4 May 2020 18:29:13 -0400 Subject: [PATCH 18/18] Framework: PHPCS formatting --- lib/compat.php | 1 + phpunit/class-block-context-test.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/compat.php b/lib/compat.php index 6c84309b017f3..48f3c7db728cf 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -206,6 +206,7 @@ function gutenberg_render_block_with_assigned_block_context( $pre_render, $parse $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 diff --git a/phpunit/class-block-context-test.php b/phpunit/class-block-context-test.php index b69d4c4d74155..a990d882e5fb6 100644 --- a/phpunit/class-block-context-test.php +++ b/phpunit/class-block-context-test.php @@ -141,7 +141,7 @@ function test_provides_default_context() { $this->register_block_type( 'gutenberg/test-context-consumer', array( - 'context' => array( 'postId', 'postType' ), + 'context' => array( 'postId', 'postType' ), 'render_callback' => function( $attributes, $content, $block ) use ( &$provided_context ) { $provided_context[] = $block->context;