Blocks: Introduce WP_Block_Type and WP_Block_Type_Registry classes.

These are the foundational classes allowing blocks to be registered and used throughout WordPress.

This commit also includes the `has_block()` and `has_blocks()` functions, which are required for unit testing these classes.

Merges [43742] from the 5.0 branch to trunk.

Props adamsilverstein, danielbachhuber, desrosj.
Fixes #45097.
See #45109.


git-svn-id: https://develop.svn.wordpress.org/trunk@44108 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Gary Pendergast 2018-12-13 09:43:29 +00:00
parent 5c9c54239d
commit 9254ae4a72
8 changed files with 974 additions and 0 deletions

View File

@ -0,0 +1,61 @@
<?php
/**
* Functions related to registering and parsing blocks.
*
* @package WordPress
* @subpackage Blocks
* @since 5.0.0
*/
/**
* Determine whether a post or content string has blocks.
*
* This test optimizes for performance rather than strict accuracy, detecting
* the pattern of a block but not validating its structure. For strict accuracy,
* you should use the block parser on post content.
*
* @since 5.0.0
* @see parse_blocks()
*
* @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. Defaults to global $post.
* @return bool Whether the post has blocks.
*/
function has_blocks( $post = null ) {
if ( ! is_string( $post ) ) {
$wp_post = get_post( $post );
if ( $wp_post instanceof WP_Post ) {
$post = $wp_post->post_content;
}
}
return false !== strpos( (string) $post, '<!-- wp:' );
}
/**
* Determine whether a $post or a string contains a specific block type.
*
* This test optimizes for performance rather than strict accuracy, detecting
* the block type exists but not validating its structure. For strict accuracy,
* you should use the block parser on post content.
*
* @since 5.0.0
* @see parse_blocks()
*
* @param string $block_type Full Block type to look for.
* @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. Defaults to global $post.
* @return bool Whether the post content contains the specified block.
*/
function has_block( $block_type, $post = null ) {
if ( ! has_blocks( $post ) ) {
return false;
}
if ( ! is_string( $post ) ) {
$wp_post = get_post( $post );
if ( $wp_post instanceof WP_Post ) {
$post = $wp_post->post_content;
}
}
return false !== strpos( $post, '<!-- wp:' . $block_type . ' ' );
}

View File

@ -0,0 +1,173 @@
<?php
/**
* Blocks API: WP_Block_Type_Registry class
*
* @package WordPress
* @subpackage Blocks
* @since 5.0.0
*/
/**
* Core class used for interacting with block types.
*
* @since 5.0.0
*/
final class WP_Block_Type_Registry {
/**
* Registered block types, as `$name => $instance` pairs.
*
* @since 5.0.0
* @var WP_Block_Type[]
*/
private $registered_block_types = array();
/**
* Container for the main instance of the class.
*
* @since 5.0.0
* @var WP_Block_Type_Registry|null
*/
private static $instance = null;
/**
* Registers a block type.
*
* @since 5.0.0
*
* @param string|WP_Block_Type $name Block type name including namespace, or alternatively a
* complete WP_Block_Type instance. In case a WP_Block_Type
* is provided, the $args parameter will be ignored.
* @param array $args {
* Optional. Array of block type arguments. Any arguments may be defined, however the
* ones described below are supported by default. Default empty array.
*
* @type callable $render_callback Callback used to render blocks of this block type.
* @type array $attributes Block attributes mapping, property name to schema.
* }
* @return WP_Block_Type|false The registered block type on success, or false on failure.
*/
public function register( $name, $args = array() ) {
$block_type = null;
if ( $name instanceof WP_Block_Type ) {
$block_type = $name;
$name = $block_type->name;
}
if ( ! is_string( $name ) ) {
$message = __( 'Block type names must be strings.' );
_doing_it_wrong( __METHOD__, $message, '5.0.0' );
return false;
}
if ( preg_match( '/[A-Z]+/', $name ) ) {
$message = __( 'Block type names must not contain uppercase characters.' );
_doing_it_wrong( __METHOD__, $message, '5.0.0' );
return false;
}
$name_matcher = '/^[a-z0-9-]+\/[a-z0-9-]+$/';
if ( ! preg_match( $name_matcher, $name ) ) {
$message = __( 'Block type names must contain a namespace prefix. Example: my-plugin/my-custom-block-type' );
_doing_it_wrong( __METHOD__, $message, '5.0.0' );
return false;
}
if ( $this->is_registered( $name ) ) {
/* translators: %s: block name */
$message = sprintf( __( 'Block type "%s" is already registered.' ), $name );
_doing_it_wrong( __METHOD__, $message, '5.0.0' );
return false;
}
if ( ! $block_type ) {
$block_type = new WP_Block_Type( $name, $args );
}
$this->registered_block_types[ $name ] = $block_type;
return $block_type;
}
/**
* Unregisters a block type.
*
* @since 5.0.0
*
* @param string|WP_Block_Type $name Block type name including namespace, or alternatively a
* complete WP_Block_Type instance.
* @return WP_Block_Type|false The unregistered block type on success, or false on failure.
*/
public function unregister( $name ) {
if ( $name instanceof WP_Block_Type ) {
$name = $name->name;
}
if ( ! $this->is_registered( $name ) ) {
/* translators: %s: block name */
$message = sprintf( __( 'Block type "%s" is not registered.' ), $name );
_doing_it_wrong( __METHOD__, $message, '5.0.0' );
return false;
}
$unregistered_block_type = $this->registered_block_types[ $name ];
unset( $this->registered_block_types[ $name ] );
return $unregistered_block_type;
}
/**
* Retrieves a registered block type.
*
* @since 5.0.0
*
* @param string $name Block type name including namespace.
* @return WP_Block_Type|null The registered block type, or null if it is not registered.
*/
public function get_registered( $name ) {
if ( ! $this->is_registered( $name ) ) {
return null;
}
return $this->registered_block_types[ $name ];
}
/**
* Retrieves all registered block types.
*
* @since 5.0.0
*
* @return WP_Block_Type[] Associative array of `$block_type_name => $block_type` pairs.
*/
public function get_all_registered() {
return $this->registered_block_types;
}
/**
* Checks if a block type is registered.
*
* @since 5.0.0
*
* @param string $name Block type name including namespace.
* @return bool True if the block type is registered, false otherwise.
*/
public function is_registered( $name ) {
return isset( $this->registered_block_types[ $name ] );
}
/**
* Utility method to retrieve the main instance of the class.
*
* The instance will be created if it does not exist yet.
*
* @since 5.0.0
*
* @return WP_Block_Type_Registry The main instance.
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
}

View File

@ -0,0 +1,205 @@
<?php
/**
* Blocks API: WP_Block_Type class
*
* @package WordPress
* @subpackage Blocks
* @since 5.0.0
*/
/**
* Core class representing a block type.
*
* @since 5.0.0
*
* @see register_block_type()
*/
class WP_Block_Type {
/**
* Block type key.
*
* @since 5.0.0
* @var string
*/
public $name;
/**
* Block type render callback.
*
* @since 5.0.0
* @var callable
*/
public $render_callback;
/**
* Block type attributes property schemas.
*
* @since 5.0.0
* @var array
*/
public $attributes;
/**
* Block type editor script handle.
*
* @since 5.0.0
* @var string
*/
public $editor_script;
/**
* Block type front end script handle.
*
* @since 5.0.0
* @var string
*/
public $script;
/**
* Block type editor style handle.
*
* @since 5.0.0
* @var string
*/
public $editor_style;
/**
* Block type front end style handle.
*
* @since 5.0.0
* @var string
*/
public $style;
/**
* Constructor.
*
* Will populate object properties from the provided arguments.
*
* @since 5.0.0
*
* @see register_block_type()
*
* @param string $block_type Block type name including namespace.
* @param array|string $args Optional. Array or string of arguments for registering a block type.
* Default empty array.
*/
public function __construct( $block_type, $args = array() ) {
$this->name = $block_type;
$this->set_props( $args );
}
/**
* Renders the block type output for given attributes.
*
* @since 5.0.0
*
* @param array $attributes Optional. Block attributes. Default empty array.
* @param string $content Optional. Block content. Default empty string.
* @return string Rendered block type output.
*/
public function render( $attributes = array(), $content = '' ) {
if ( ! $this->is_dynamic() ) {
return '';
}
$attributes = $this->prepare_attributes_for_render( $attributes );
return (string) call_user_func( $this->render_callback, $attributes, $content );
}
/**
* Returns true if the block type is dynamic, or false otherwise. A dynamic
* block is one which defers its rendering to occur on-demand at runtime.
*
* @since 5.0.0
*
* @return boolean Whether block type is dynamic.
*/
public function is_dynamic() {
return is_callable( $this->render_callback );
}
/**
* Validates attributes against the current block schema, populating
* defaulted and missing values, and omitting unknown attributes.
*
* @since 5.0.0
*
* @param array $attributes Original block attributes.
* @return array Prepared block attributes.
*/
public function prepare_attributes_for_render( $attributes ) {
if ( ! isset( $this->attributes ) ) {
return $attributes;
}
$prepared_attributes = array();
foreach ( $this->attributes as $attribute_name => $schema ) {
$value = null;
if ( isset( $attributes[ $attribute_name ] ) ) {
$is_valid = rest_validate_value_from_schema( $attributes[ $attribute_name ], $schema );
if ( ! is_wp_error( $is_valid ) ) {
$value = rest_sanitize_value_from_schema( $attributes[ $attribute_name ], $schema );
}
}
if ( is_null( $value ) && isset( $schema['default'] ) ) {
$value = $schema['default'];
}
$prepared_attributes[ $attribute_name ] = $value;
}
return $prepared_attributes;
}
/**
* Sets block type properties.
*
* @since 5.0.0
*
* @param array|string $args Array or string of arguments for registering a block type.
*/
public function set_props( $args ) {
$args = wp_parse_args(
$args,
array(
'render_callback' => null,
)
);
$args['name'] = $this->name;
foreach ( $args as $property_name => $property_value ) {
$this->$property_name = $property_value;
}
}
/**
* Get all available block attributes including possible layout attribute from Columns block.
*
* @since 5.0.0
*
* @return array Array of attributes.
*/
public function get_attributes() {
return is_array( $this->attributes ) ?
array_merge(
$this->attributes,
array(
'layout' => array(
'type' => 'string',
),
)
) :
array(
'layout' => array(
'type' => 'string',
),
);
}
}

View File

@ -244,6 +244,9 @@ require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-term-meta-fields.php'
require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-user-meta-fields.php' ); require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-user-meta-fields.php' );
require( ABSPATH . WPINC . '/rest-api/search/class-wp-rest-search-handler.php' ); require( ABSPATH . WPINC . '/rest-api/search/class-wp-rest-search-handler.php' );
require( ABSPATH . WPINC . '/rest-api/search/class-wp-rest-post-search-handler.php' ); require( ABSPATH . WPINC . '/rest-api/search/class-wp-rest-post-search-handler.php' );
require( ABSPATH . WPINC . '/class-wp-block-type.php' );
require( ABSPATH . WPINC . '/class-wp-block-type-registry.php' );
require( ABSPATH . WPINC . '/blocks.php' );
$GLOBALS['wp_embed'] = new WP_Embed(); $GLOBALS['wp_embed'] = new WP_Embed();

View File

@ -133,6 +133,7 @@ require dirname( __FILE__ ) . '/exceptions.php';
require dirname( __FILE__ ) . '/utils.php'; require dirname( __FILE__ ) . '/utils.php';
require dirname( __FILE__ ) . '/spy-rest-server.php'; require dirname( __FILE__ ) . '/spy-rest-server.php';
require dirname( __FILE__ ) . '/class-wp-rest-test-search-handler.php'; require dirname( __FILE__ ) . '/class-wp-rest-test-search-handler.php';
require dirname( __FILE__ ) . '/class-wp-fake-block-type.php';
/** /**
* A child class of the PHP test runner. * A child class of the PHP test runner.

View File

@ -0,0 +1,27 @@
<?php
/**
* WP_Fake_Block_Type for testing
*
* @package WordPress
* @subpackage Blocks
* @since 5.0.0
*/
/**
* Test class extending WP_Block_Type
*
* @since 5.0.0
*/
class WP_Fake_Block_Type extends WP_Block_Type {
/**
* Render the fake block.
*
* @param array $attributes Optional. Block attributes. Default empty array.
* @param string $content Optional. Block content. Default empty string.
* @return string Rendered block HTML.
*/
public function render( $attributes = array(), $content = '' ) {
return '<div>' . $content . '</div>';
}
}

View File

@ -0,0 +1,191 @@
<?php
/**
* WP_Block_Type_Registry Tests
*
* @package WordPress
* @subpackage Blocks
* @since 5.0.0
*/
/**
* Tests for WP_Block_Type_Registry
*
* @since 5.0.0
*
* @group blocks
*/
class WP_Test_Block_Type_Registry extends WP_UnitTestCase {
/**
* Fake block type registry.
*
* @since 5.0.0
* @var WP_Block_Type_Registry
*/
private $registry = null;
/**
* Set up each test method.
*
* @since 5.0.0
*/
public function setUp() {
parent::setUp();
$this->registry = new WP_Block_Type_Registry();
}
/**
* Tear down each test method.
*
* @since 5.0.0
*/
public function tearDown() {
parent::tearDown();
$this->registry = null;
}
/**
* Should reject numbers
*
* @ticket 45097
*
* @expectedIncorrectUsage WP_Block_Type_Registry::register
*/
public function test_invalid_non_string_names() {
$result = $this->registry->register( 1, array() );
$this->assertFalse( $result );
}
/**
* Should reject blocks without a namespace
*
* @ticket 45097
*
* @expectedIncorrectUsage WP_Block_Type_Registry::register
*/
public function test_invalid_names_without_namespace() {
$result = $this->registry->register( 'paragraph', array() );
$this->assertFalse( $result );
}
/**
* Should reject blocks with invalid characters
*
* @ticket 45097
*
* @expectedIncorrectUsage WP_Block_Type_Registry::register
*/
public function test_invalid_characters() {
$result = $this->registry->register( 'still/_doing_it_wrong', array() );
$this->assertFalse( $result );
}
/**
* Should reject blocks with uppercase characters
*
* @ticket 45097
*
* @expectedIncorrectUsage WP_Block_Type_Registry::register
*/
public function test_uppercase_characters() {
$result = $this->registry->register( 'Core/Paragraph', array() );
$this->assertFalse( $result );
}
/**
* Should accept valid block names
*
* @ticket 45097
*/
public function test_register_block_type() {
$name = 'core/paragraph';
$settings = array(
'icon' => 'editor-paragraph',
);
$block_type = $this->registry->register( $name, $settings );
$this->assertEquals( $name, $block_type->name );
$this->assertEquals( $settings['icon'], $block_type->icon );
$this->assertEquals( $block_type, $this->registry->get_registered( $name ) );
}
/**
* Should fail to re-register the same block
*
* @ticket 45097
*
* @expectedIncorrectUsage WP_Block_Type_Registry::register
*/
public function test_register_block_type_twice() {
$name = 'core/paragraph';
$settings = array(
'icon' => 'editor-paragraph',
);
$result = $this->registry->register( $name, $settings );
$this->assertNotFalse( $result );
$result = $this->registry->register( $name, $settings );
$this->assertFalse( $result );
}
/**
* Should accept a WP_Block_Type instance
*
* @ticket 45097
*/
public function test_register_block_type_instance() {
$block_type = new WP_Fake_Block_Type( 'core/fake' );
$result = $this->registry->register( $block_type );
$this->assertSame( $block_type, $result );
}
/**
* Unregistering should fail if a block is not registered
*
* @ticket 45097
*
* @expectedIncorrectUsage WP_Block_Type_Registry::unregister
*/
public function test_unregister_not_registered_block() {
$result = $this->registry->unregister( 'core/unregistered' );
$this->assertFalse( $result );
}
/**
* Should unregister existing blocks
*
* @ticket 45097
*/
public function test_unregister_block_type() {
$name = 'core/paragraph';
$settings = array(
'icon' => 'editor-paragraph',
);
$this->registry->register( $name, $settings );
$block_type = $this->registry->unregister( $name );
$this->assertEquals( $name, $block_type->name );
$this->assertEquals( $settings['icon'], $block_type->icon );
$this->assertFalse( $this->registry->is_registered( $name ) );
}
/**
* @ticket 45097
*/
public function test_get_all_registered() {
$names = array( 'core/paragraph', 'core/image', 'core/blockquote' );
$settings = array(
'icon' => 'random',
);
foreach ( $names as $name ) {
$this->registry->register( $name, $settings );
}
$registered = $this->registry->get_all_registered();
$this->assertEqualSets( $names, array_keys( $registered ) );
}
}

View File

@ -0,0 +1,313 @@
<?php
/**
* WP_Block_Type Tests
*
* @package WordPress
* @subpackage Blocks
* @since 5.0.0
*/
/**
* Tests for WP_Block_Type
*
* @since 5.0.0
*
* @group blocks
*/
class WP_Test_Block_Type extends WP_UnitTestCase {
/**
* Editor user ID.
*
* @since 5.0.0
* @var int
*/
protected static $editor_user_id;
/**
* ID for a post containing blocks.
*
* @since 5.0.0
* @var int
*/
protected static $post_with_blocks;
/**
* ID for a post without blocks.
*
* @since 5.0.0
* @var int
*/
protected static $post_without_blocks;
/**
* Set up before class.
*
* @since 5.0.0
*/
public static function wpSetUpBeforeClass() {
self::$editor_user_id = self::factory()->user->create(
array(
'role' => 'editor',
)
);
self::$post_with_blocks = self::factory()->post->create(
array(
'post_title' => 'Example',
'post_content' => "<!-- wp:core/text {\"dropCap\":true} -->\n<p class=\"has-drop-cap\">Tester</p>\n<!-- /wp:core/text -->",
)
);
self::$post_without_blocks = self::factory()->post->create(
array(
'post_title' => 'Example',
'post_content' => 'Tester',
)
);
}
/**
* @ticket 45097
*/
public function test_set_props() {
$name = 'core/fake';
$args = array(
'render_callback' => array( $this, 'render_fake_block' ),
'foo' => 'bar',
);
$block_type = new WP_Block_Type( $name, $args );
$this->assertSame( $name, $block_type->name );
$this->assertSame( $args['render_callback'], $block_type->render_callback );
$this->assertSame( $args['foo'], $block_type->foo );
}
/**
* @ticket 45097
*/
public function test_render() {
$attributes = array(
'foo' => 'bar',
'bar' => 'foo',
);
$block_type = new WP_Block_Type(
'core/fake',
array(
'render_callback' => array( $this, 'render_fake_block' ),
)
);
$output = $block_type->render( $attributes );
$this->assertEquals( $attributes, json_decode( $output, true ) );
}
/**
* @ticket 45097
*/
public function test_render_with_content() {
$attributes = array(
'foo' => 'bar',
'bar' => 'foo',
);
$content = 'baz';
$expected = array_merge( $attributes, array( '_content' => $content ) );
$block_type = new WP_Block_Type(
'core/fake',
array(
'render_callback' => array( $this, 'render_fake_block_with_content' ),
)
);
$output = $block_type->render( $attributes, $content );
$this->assertEquals( $expected, json_decode( $output, true ) );
}
/**
* @ticket 45097
*/
public function test_render_for_static_block() {
$block_type = new WP_Block_Type( 'core/fake', array() );
$output = $block_type->render();
$this->assertEquals( '', $output );
}
/**
* @ticket 45097
*/
public function test_is_dynamic_for_static_block() {
$block_type = new WP_Block_Type( 'core/fake', array() );
$this->assertFalse( $block_type->is_dynamic() );
}
/**
* @ticket 45097
*/
public function test_is_dynamic_for_dynamic_block() {
$block_type = new WP_Block_Type(
'core/fake',
array(
'render_callback' => array( $this, 'render_fake_block' ),
)
);
$this->assertTrue( $block_type->is_dynamic() );
}
/**
* @ticket 45097
*/
public function test_prepare_attributes() {
$attributes = array(
'correct' => 'include',
'wrongType' => 5,
'wrongTypeDefaulted' => 5,
/* missingDefaulted */
'undefined' => 'omit',
);
$block_type = new WP_Block_Type(
'core/fake',
array(
'attributes' => array(
'correct' => array(
'type' => 'string',
),
'wrongType' => array(
'type' => 'string',
),
'wrongTypeDefaulted' => array(
'type' => 'string',
'default' => 'defaulted',
),
'missingDefaulted' => array(
'type' => 'string',
'default' => 'define',
),
),
)
);
$prepared_attributes = $block_type->prepare_attributes_for_render( $attributes );
$this->assertEquals(
array(
'correct' => 'include',
'wrongType' => null,
'wrongTypeDefaulted' => 'defaulted',
'missingDefaulted' => 'define',
),
$prepared_attributes
);
}
/**
* @ticket 45097
*/
public function test_has_block_with_mixed_content() {
$mixed_post_content = 'before' .
'<!-- wp:core/fake --><!-- /wp:core/fake -->' .
'<!-- wp:core/fake_atts {"value":"b1"} --><!-- /wp:core/fake_atts -->' .
'<!-- wp:core/fake-child -->
<p>testing the test</p>
<!-- /wp:core/fake-child -->' .
'between' .
'<!-- wp:core/self-close-fake /-->' .
'<!-- wp:custom/fake {"value":"b2"} /-->' .
'after';
$this->assertTrue( has_block( 'core/fake', $mixed_post_content ) );
$this->assertTrue( has_block( 'core/fake_atts', $mixed_post_content ) );
$this->assertTrue( has_block( 'core/fake-child', $mixed_post_content ) );
$this->assertTrue( has_block( 'core/self-close-fake', $mixed_post_content ) );
$this->assertTrue( has_block( 'custom/fake', $mixed_post_content ) );
// checking for a partial block name should fail.
$this->assertFalse( has_block( 'core/fak', $mixed_post_content ) );
// checking for a wrong namespace should fail.
$this->assertFalse( has_block( 'custom/fake_atts', $mixed_post_content ) );
// checking for namespace only should not work. Or maybe ... ?
$this->assertFalse( has_block( 'core', $mixed_post_content ) );
}
/**
* @ticket 45097
*/
public function test_has_block_with_invalid_content() {
// some content with invalid HMTL comments and a single valid block.
$invalid_content = 'before' .
'<!- - wp:core/weird-space --><!-- /wp:core/weird-space -->' .
'<!--wp:core/untrimmed-left --><!-- /wp:core/untrimmed -->' .
'<!-- wp:core/fake --><!-- /wp:core/fake -->' .
'<!-- wp:core/untrimmed-right--><!-- /wp:core/untrimmed2 -->' .
'after';
$this->assertFalse( has_block( 'core/text', self::$post_without_blocks ) );
$this->assertFalse( has_block( 'core/weird-space', $invalid_content ) );
$this->assertFalse( has_block( 'core/untrimmed-left', $invalid_content ) );
$this->assertFalse( has_block( 'core/untrimmed-right', $invalid_content ) );
$this->assertTrue( has_block( 'core/fake', $invalid_content ) );
}
/**
* @ticket 45097
*/
public function test_post_has_block() {
// should fail for a non-existent block `custom/fake`.
$this->assertFalse( has_block( 'custom/fake', self::$post_with_blocks ) );
// this functions should not work without the second param until the $post global is set.
$this->assertFalse( has_block( 'core/text' ) );
$this->assertFalse( has_block( 'core/fake' ) );
global $post;
$post = get_post( self::$post_with_blocks );
// check if the function correctly detects content from the $post global.
$this->assertTrue( has_block( 'core/text' ) );
// even if it detects a proper $post global it should still be false for a missing block.
$this->assertFalse( has_block( 'core/fake' ) );
}
/**
* Renders a test block without content.
*
* @since 5.0.0
*
* @param array $attributes Block attributes. Default empty array.
* @return string JSON encoded list of attributes.
*/
public function render_fake_block( $attributes ) {
return json_encode( $attributes );
}
/**
* Renders a test block with content.
*
* @since 5.0.0
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @return string JSON encoded list of attributes.
*/
public function render_fake_block_with_content( $attributes, $content ) {
$attributes['_content'] = $content;
return json_encode( $attributes );
}
}