diff --git a/src/wp-includes/class-wp-block-type.php b/src/wp-includes/class-wp-block-type.php index 9caa366032a75..6ad8ebcd6790e 100644 --- a/src/wp-includes/class-wp-block-type.php +++ b/src/wp-includes/class-wp-block-type.php @@ -113,9 +113,18 @@ class WP_Block_Type { * Block variations. * * @since 5.8.0 - * @var array[] + * @since 6.5.0 Only accessible through magic getter. null by default. + * @var array[]|null */ - public $variations = array(); + private $variations = null; + + /** + * Block variations callback. + * + * @since 6.5.0 + * @var callable|null + */ + public $variation_callback = null; /** * Custom CSS selectors for theme.json style generation. @@ -296,6 +305,7 @@ class WP_Block_Type { * @type array|null $supports Supported features. * @type array|null $example Structured data for the block preview. * @type callable|null $render_callback Block type render callback. + * @type callable|null $variation_callback Block type variations callback. * @type array|null $attributes Block type attributes property schemas. * @type string[] $uses_context Context values inherited by blocks of this type. * @type string[]|null $provides_context Context provided by blocks of this type. @@ -325,6 +335,10 @@ public function __construct( $block_type, $args = array() ) { * null when value not found, or void when unknown property name provided. */ public function __get( $name ) { + if ( 'variations' === $name ) { + return $this->get_variations(); + } + if ( ! in_array( $name, $this->deprecated_properties, true ) ) { return; } @@ -353,6 +367,10 @@ public function __get( $name ) { * or false otherwise. */ public function __isset( $name ) { + if ( 'variations' === $name ) { + return true; + } + if ( ! in_array( $name, $this->deprecated_properties, true ) ) { return false; } @@ -372,6 +390,11 @@ public function __isset( $name ) { * @param mixed $value Property value. */ public function __set( $name, $value ) { + if ( 'variations' === $name ) { + $this->variations = $value; + return; + } + if ( ! in_array( $name, $this->deprecated_properties, true ) ) { $this->{$name} = $value; return; @@ -540,4 +563,30 @@ public function get_attributes() { $this->attributes : array(); } + + /** + * Get block variations. + * + * @since 6.5.0 + * + * @return array[] + */ + public function get_variations() { + if ( ! isset( $this->variations ) ) { + $this->variations = array(); + if ( is_callable( $this->variation_callback ) ) { + $this->variations = call_user_func( $this->variation_callback ); + } + } + + /** + * Filters the registered variations for a block type. + * + * @since 6.5.0 + * + * @param array $variations Array of registered variations for a block type. + * @param WP_Block_Type $block_type The full block type object. + */ + return apply_filters( 'get_block_type_variations', $this->variations, $this ); + } } diff --git a/tests/phpunit/tests/blocks/wpBlockType.php b/tests/phpunit/tests/blocks/wpBlockType.php index d2c7a13e6b345..b826faf575946 100644 --- a/tests/phpunit/tests/blocks/wpBlockType.php +++ b/tests/phpunit/tests/blocks/wpBlockType.php @@ -460,4 +460,196 @@ public function data_block_version() { array( '', 0 ), ); } + + /** + * @ticket 59969 + */ + public function test_variation_callback() { + $block_type = new WP_Block_Type( + 'test/block', + array( + 'title' => 'Test title', + 'variation_callback' => array( $this, 'mock_variation_callback' ), + ) + ); + + $this->assertSameSets( $this->mock_variation_callback(), $block_type->variations ); + } + + /** + * @ticket 59969 + * @covers WP_Block_Type::get_variations + */ + public function test_get_variations() { + $block_type = new WP_Block_Type( + 'test/block', + array( + 'title' => 'Test title', + 'variation_callback' => array( $this, 'mock_variation_callback' ), + ) + ); + + $this->assertSameSets( $this->mock_variation_callback(), $block_type->get_variations() ); + } + + /** + * @ticket 59969 + */ + public function test_variations_precedence_over_callback() { + $test_variations = array( 'name' => 'test1' ); + + $block_type = new WP_Block_Type( + 'test/block', + array( + 'title' => 'Test title', + 'variations' => $test_variations, + 'variation_callback' => array( $this, 'mock_variation_callback' ), + ) + ); + + // If the variations are defined, the callback should not be used. + $this->assertSameSets( $test_variations, $block_type->variations ); + } + + /** + * @ticket 59969 + */ + public function test_variations_callback_are_lazy_loaded() { + $callback_called = false; + + $block_type = new WP_Block_Type( + 'test/block', + array( + 'title' => 'Test title', + 'variation_callback' => function () use ( &$callback_called ) { + $callback_called = true; + return $this->mock_variation_callback(); + }, + ) + ); + + $this->assertSame( false, $callback_called, 'The callback should not be called before the variations are accessed.' ); + $block_type->variations; // access the variations. + $this->assertSame( true, $callback_called, 'The callback should be called when the variations are accessed.' ); + } + + /** + * @ticket 59969 + * @covers WP_Block_Type::get_variations + */ + public function test_variations_precedence_over_callback_post_registration() { + $test_variations = array( 'name' => 'test1' ); + $callback_called = false; + + $block_type = new WP_Block_Type( + 'test/block', + array( + 'title' => 'Test title', + 'variation_callback' => function () use ( &$callback_called ) { + $callback_called = true; + return $this->mock_variation_callback(); + }, + ) + ); + $block_type->variations = $test_variations; + + // If the variations are defined after registration but before first access, the callback should not override it. + $this->assertSameSets( $test_variations, $block_type->get_variations(), 'Variations are same as variations set' ); + $this->assertSame( false, $callback_called, 'The callback was never called.' ); + } + + /** + * @ticket 59969 + * @covers WP_Block_Type::get_variations + */ + public function test_variations_callback_happens_only_once() { + $callback_count = 0; + + $block_type = new WP_Block_Type( + 'test/block', + array( + 'title' => 'Test title', + 'variation_callback' => function () use ( &$callback_count ) { + $callback_count++; + return $this->mock_variation_callback(); + }, + ) + ); + + $this->assertSame( 0, $callback_count, 'The callback should not be called before the variations are accessed.' ); + $block_type->get_variations(); // access the variations. + $this->assertSame( 1, $callback_count, 'The callback should be called when the variations are accessed.' ); + $block_type->get_variations(); // access the variations again. + $this->assertSame( 1, $callback_count, 'The callback should not be called again.' ); + } + + /** + * Test filter function for get_block_type_variations filter. + * + * @param array $variations Block variations before filter. + * @param WP_Block_Type $block_type Block type. + * + * @return array Block variations after filter. + */ + public function filter_test_variations( $variations, $block_type ) { + return array( array( 'name' => 'test1' ) ); + } + + /** + * @ticket 59969 + */ + public function test_get_block_type_variations_filter_with_variation_callback() { + // Filter will override the variations obtained from the callback. + add_filter( 'get_block_type_variations', array( $this, 'filter_test_variations' ), 10, 2 ); + $expected_variations = array( array( 'name' => 'test1' ) ); + + $callback_called = false; + $block_type = new WP_Block_Type( + 'test/block', + array( + 'title' => 'Test title', + 'variation_callback' => function () use ( &$callback_called ) { + $callback_called = true; + return $this->mock_variation_callback(); + }, + ) + ); + + $obtained_variations = $block_type->variations; // access the variations. + + $this->assertSame( true, $callback_called, 'The callback should be called when the variations are accessed.' ); + $this->assertSameSets( $obtained_variations, $expected_variations, 'The variations obtained from the callback should be filtered.' ); + } + + /** + * @ticket 59969 + */ + public function test_get_block_type_variations_filter_variations() { + // Filter will override the variations set during registration. + add_filter( 'get_block_type_variations', array( $this, 'filter_test_variations' ), 10, 2 ); + $expected_variations = array( array( 'name' => 'test1' ) ); + + $block_type = new WP_Block_Type( + 'test/block', + array( + 'title' => 'Test title', + 'variations' => $this->mock_variation_callback(), + ) + ); + + $obtained_variations = $block_type->variations; // access the variations. + $this->assertSameSets( $obtained_variations, $expected_variations, 'The variations that was initially set should be filtered.' ); + } + + /** + * Mock variation callback. + * + * @return array + */ + public function mock_variation_callback() { + return array( + array( 'name' => 'var1' ), + array( 'name' => 'var2' ), + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-block-type-controller.php b/tests/phpunit/tests/rest-api/rest-block-type-controller.php index 8b49d2c034a20..59c77e03b68b2 100644 --- a/tests/phpunit/tests/rest-api/rest-block-type-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-type-controller.php @@ -734,6 +734,35 @@ protected function check_block_type_object( $block_type, $data, $links ) { } } + /** + * @ticket 59969 + */ + public function test_variation_callback() { + $block_type = 'test/block'; + $settings = array( + 'title' => true, + 'variation_callback' => array( $this, 'mock_variation_callback' ), + ); + register_block_type( $block_type, $settings ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/block-types/' . $block_type ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSameSets( $this->mock_variation_callback(), $data['variations'] ); + } + + /** + * Mock variation callback. + * + * @return array + */ + public function mock_variation_callback() { + return array( + array( 'name' => 'var1' ), + array( 'name' => 'var2' ), + ); + } + /** * The create_item() method does not exist for block types. *