<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[57373] trunk: Editor: Add registry for block binding sources</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/57373">57373</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/57373","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>gziolo</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-01-29 11:14:22 +0000 (Mon, 29 Jan 2024)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Editor: Add registry for block binding sources
It is part of the sync from the Gutenberg plugin that introduces the registry for block binding sources required for the new Block Bindings API: https://github.com/WordPress/gutenberg/issues/54536.
See <a href="https://core.trac.wordpress.org/ticket/60282">#60282</a>.
Props czapla, artemiosans, santosguillamot, sc0ttkclark, lgladdy, talldanwp, swissspidy, youknowriad, fabiankaegy.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpsettingsphp">trunk/src/wp-settings.php</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesclasswpblockbindingsregistryphp">trunk/src/wp-includes/class-wp-block-bindings-registry.php</a></li>
<li>trunk/tests/phpunit/tests/block-bindings/</li>
<li><a href="#trunktestsphpunittestsblockbindingswpBlockBindingsRegistryphp">trunk/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesclasswpblockbindingsregistryphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/class-wp-block-bindings-registry.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-block-bindings-registry.php (rev 0)
+++ trunk/src/wp-includes/class-wp-block-bindings-registry.php 2024-01-29 11:14:22 UTC (rev 57373)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,194 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Block Bindings API: WP_Block_Bindings_Registry class.
+ *
+ * Supports overriding content in blocks by connecting them to different sources.
+ *
+ * @package WordPress
+ * @subpackage Block Bindings
+ * @since 6.5.0
+ */
+
+/**
+ * Core class used for interacting with block binding sources.
+ *
+ * @since 6.5.0
+ */
+final class WP_Block_Bindings_Registry {
+
+ /**
+ * Holds the registered block bindings sources, keyed by source identifier.
+ *
+ * @since 6.5.0
+ *
+ * @var array
+ */
+ private $sources = array();
+
+ /**
+ * Container for the main instance of the class.
+ *
+ * @since 6.5.0
+ * @var WP_Block_Bindings_Registry|null
+ */
+ private static $instance = null;
+
+ /**
+ * Registers a new block binding source.
+ *
+ * Sources are used to override block's original attributes with a value
+ * coming from the source. Once a source is registered, it can be used by a
+ * block by setting its `metadata.bindings` attribute to a value that refers
+ * to the source.
+ *
+ * @since 6.5.0
+ *
+ * @param string $source_name The name of the source.
+ * @param array $source_properties {
+ * The array of arguments that are used to register a source.
+ *
+ * @type string $label The label of the source.
+ * @type callback $get_value_callback A callback executed when the source is processed during block rendering.
+ * The callback should have the following signature:
+ *
+ * `function ($source_args, $block_instance,$attribute_name): mixed`
+ * - @param array $source_args Array containing source arguments
+ * used to look up the override value,
+ * i.e. {"key": "foo"}.
+ * - @param WP_Block $block_instance The block instance.
+ * - @param string $attribute_name The name of an attribute .
+ * The callback has a mixed return type; it may return a string to override
+ * the block's original value, null, false to remove an attribute, etc.
+ * }
+ * @return array|false Source when the registration was successful, or `false` on failure.
+ */
+ public function register( $source_name, array $source_properties ) {
+ if ( ! is_string( $source_name ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ __( 'Block binding source name must be a string.' ),
+ '6.5.0'
+ );
+ return false;
+ }
+
+ if ( preg_match( '/[A-Z]+/', $source_name ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ __( 'Block binding source names must not contain uppercase characters.' ),
+ '6.5.0'
+ );
+ return false;
+ }
+
+ $name_matcher = '/^[a-z0-9-]+\/[a-z0-9-]+$/';
+ if ( ! preg_match( $name_matcher, $source_name ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ __( 'Block binding source names must contain a namespace prefix. Example: my-plugin/my-custom-source' ),
+ '6.5.0'
+ );
+ return false;
+ }
+
+ if ( $this->is_registered( $source_name ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ /* translators: %s: Block binding source name. */
+ sprintf( __( 'Block binding source "%s" already registered.' ), $source_name ),
+ '6.5.0'
+ );
+ return false;
+ }
+
+ $source = array_merge(
+ array( 'name' => $source_name ),
+ $source_properties
+ );
+
+ $this->sources[ $source_name ] = $source;
+
+ return $source;
+ }
+
+ /**
+ * Unregisters a block binding source.
+ *
+ * @since 6.5.0
+ *
+ * @param string $source_name Block binding source name including namespace.
+ * @return array|false The unregistred block binding source on success and `false` otherwise.
+ */
+ public function unregister( $source_name ) {
+ if ( ! $this->is_registered( $source_name ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ /* translators: %s: Block binding source name. */
+ sprintf( __( 'Block binding "%s" not found.' ), $source_name ),
+ '6.5.0'
+ );
+ return false;
+ }
+
+ $unregistered_source = $this->sources[ $source_name ];
+ unset( $this->sources[ $source_name ] );
+
+ return $unregistered_source;
+ }
+
+ /**
+ * Retrieves the list of all registered block bindings sources.
+ *
+ * @since 6.5.0
+ *
+ * @return array The array of registered sources.
+ */
+ public function get_all_registered() {
+ return $this->sources;
+ }
+
+ /**
+ * Retrieves a registered block bindings source.
+ *
+ * @since 6.5.0
+ *
+ * @param string $source_name The name of the source.
+ * @return array|null The registered block binding source, or `null` if it is not registered.
+ */
+ public function get_registered( $source_name ) {
+ if ( ! $this->is_registered( $source_name ) ) {
+ return null;
+ }
+
+ return $this->sources[ $source_name ];
+ }
+
+ /**
+ * Checks if a block binding source is registered.
+ *
+ * @since 6.5.0
+ *
+ * @param string $source_name The name of the source.
+ * @return bool `true` if the block binding source is registered, `false` otherwise.
+ */
+ public function is_registered( $source_name ) {
+ return isset( $this->sources[ $source_name ] );
+ }
+
+ /**
+ * Utility method to retrieve the main instance of the class.
+ *
+ * The instance will be created if it does not exist yet.
+ *
+ * @since 6.5.0
+ *
+ * @return WP_Block_Bindings_Registry The main instance.
+ */
+ public static function get_instance() {
+ if ( null === self::$instance ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/class-wp-block-bindings-registry.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2024-01-28 21:33:00 UTC (rev 57372)
+++ trunk/src/wp-settings.php 2024-01-29 11:14:22 UTC (rev 57373)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -330,6 +330,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/sitemaps/providers/class-wp-sitemaps-taxonomies.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/sitemaps/providers/class-wp-sitemaps-users.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-block-editor-context.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/class-wp-block-bindings-registry.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-block-type.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-block-pattern-categories-registry.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-block-patterns-registry.php';
</span></span></pre></div>
<a id="trunktestsphpunittestsblockbindingswpBlockBindingsRegistryphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php (rev 0)
+++ trunk/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php 2024-01-29 11:14:22 UTC (rev 57373)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,262 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for WP_Block_Bindings_Registry.
+ *
+ * @package WordPress
+ * @subpackage Blocks
+ * @since 6.5.0
+ *
+ * @group blocks
+ * @group block-bindings
+ *
+ * @coversDefaultClass WP_Block_Bindings_Registry
+ */
+class Tests_Blocks_wpBlockBindingsRegistry extends WP_UnitTestCase {
+
+ const TEST_SOURCE_NAME = 'test/source';
+ const TEST_SOURCE_PROPERTIES = array(
+ 'label' => 'Test source',
+ );
+
+ /**
+ * Fake block bindings registry.
+ *
+ * @since 6.5.0
+ * @var WP_Block_Bindings_Registry
+ */
+ private $registry = null;
+
+ /**
+ * Set up each test method.
+ *
+ * @since 6.5.0
+ */
+ public function set_up() {
+ parent::set_up();
+
+ $this->registry = new WP_Block_Bindings_Registry();
+ }
+
+ /**
+ * Tear down each test method.
+ *
+ * @since 6.5.0
+ */
+ public function tear_down() {
+ $this->registry = null;
+
+ parent::tear_down();
+ }
+
+ /**
+ * Should reject numbers as block binding source name.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::register
+ *
+ * @expectedIncorrectUsage WP_Block_Bindings_Registry::register
+ */
+ public function test_register_invalid_non_string_names() {
+ $result = $this->registry->register( 1, self::TEST_SOURCE_PROPERTIES );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * Should reject block binding source name without a namespace.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::register
+ *
+ * @expectedIncorrectUsage WP_Block_Bindings_Registry::register
+ */
+ public function test_register_invalid_names_without_namespace() {
+ $result = $this->registry->register( 'post-meta', self::TEST_SOURCE_PROPERTIES );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * Should reject block binding source name with invalid characters.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::register
+ *
+ * @expectedIncorrectUsage WP_Block_Bindings_Registry::register
+ */
+ public function test_register_invalid_characters() {
+ $result = $this->registry->register( 'still/_doing_it_wrong', array() );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * Should reject block binding source name with uppercase characters.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::register
+ *
+ * @expectedIncorrectUsage WP_Block_Bindings_Registry::register
+ */
+ public function test_register_invalid_uppercase_characters() {
+ $result = $this->registry->register( 'Core/PostMeta', self::TEST_SOURCE_PROPERTIES );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * Should accept valid block binding source.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::register
+ */
+ public function test_register_block_binding_source() {
+ $result = $this->registry->register( self::TEST_SOURCE_NAME, self::TEST_SOURCE_PROPERTIES );
+ $this->assertSame(
+ array_merge(
+ array( 'name' => self::TEST_SOURCE_NAME ),
+ self::TEST_SOURCE_PROPERTIES
+ ),
+ $result
+ );
+ }
+
+ /**
+ * Unregistering should fail if a block binding source is not registered.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::unregister
+ *
+ * @expectedIncorrectUsage WP_Block_Bindings_Registry::unregister
+ */
+ public function test_unregister_not_registered_block() {
+ $result = $this->registry->unregister( 'test/unregistered' );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * Should unregister existing block binding source.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::unregister
+ */
+ public function test_unregister_block_source() {
+ $this->registry->register( self::TEST_SOURCE_NAME, self::TEST_SOURCE_PROPERTIES );
+
+ $result = $this->registry->unregister( self::TEST_SOURCE_NAME );
+ $this->assertSame(
+ array_merge(
+ array( 'name' => self::TEST_SOURCE_NAME ),
+ self::TEST_SOURCE_PROPERTIES
+ ),
+ $result
+ );
+ }
+
+ /**
+ * Should find all registered sources.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::register
+ * @covers WP_Block_Bindings_Registry::get_all_registered
+ */
+ public function test_get_all_registered() {
+ $source_one_name = 'test/source-one';
+ $source_one_properties = self::TEST_SOURCE_PROPERTIES;
+ $this->registry->register( $source_one_name, $source_one_properties );
+
+ $source_two_name = 'test/source-two';
+ $source_two_properties = self::TEST_SOURCE_PROPERTIES;
+ $this->registry->register( $source_two_name, $source_two_properties );
+
+ $source_three_name = 'test/source-three';
+ $source_three_properties = self::TEST_SOURCE_PROPERTIES;
+ $this->registry->register( $source_three_name, $source_three_properties );
+
+ $expected = array(
+ $source_one_name => array_merge( array( 'name' => $source_one_name ), $source_one_properties ),
+ $source_two_name => array_merge( array( 'name' => $source_two_name ), $source_two_properties ),
+ $source_three_name => array_merge( array( 'name' => $source_three_name ), $source_three_properties ),
+ );
+
+ $registered = $this->registry->get_all_registered();
+ $this->assertSame( $expected, $registered );
+ }
+
+ /**
+ * Should not find source that's not registered.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::register
+ * @covers WP_Block_Bindings_Registry::get_registered
+ */
+ public function test_get_registered_rejects_unknown_source_name() {
+ $this->registry->register( self::TEST_SOURCE_NAME, self::TEST_SOURCE_PROPERTIES );
+
+ $source = $this->registry->get_registered( 'test/unknown-source' );
+ $this->assertNull( $source );
+ }
+
+ /**
+ * Should find registered block binding source by name.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::register
+ * @covers WP_Block_Bindings_Registry::get_registered
+ */
+ public function test_get_registered() {
+ $source_one_name = 'test/source-one';
+ $source_one_properties = self::TEST_SOURCE_PROPERTIES;
+ $this->registry->register( $source_one_name, $source_one_properties );
+
+ $source_two_name = 'test/source-two';
+ $source_two_properties = self::TEST_SOURCE_PROPERTIES;
+ $this->registry->register( $source_two_name, $source_two_properties );
+
+ $source_three_name = 'test/source-three';
+ $source_three_properties = self::TEST_SOURCE_PROPERTIES;
+ $this->registry->register( $source_three_name, $source_three_properties );
+
+ $result = $this->registry->get_registered( 'test/source-two' );
+ $this->assertSame(
+ array_merge(
+ array( 'name' => $source_two_name ),
+ $source_two_properties
+ ),
+ $result
+ );
+ }
+
+ /**
+ * Should return false for source that's not registered.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::is_registered
+ */
+ public function test_is_registered_for_unknown_source() {
+ $result = $this->registry->is_registered( 'test/one' );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * Should return true if source is registered.
+ *
+ * @ticket 60282
+ *
+ * @covers WP_Block_Bindings_Registry::register
+ * @covers WP_Block_Bindings_Registry::is_registered
+ */
+ public function test_is_registered_for_known_source() {
+ $this->registry->register( self::TEST_SOURCE_NAME, self::TEST_SOURCE_PROPERTIES );
+
+ $result = $this->registry->is_registered( self::TEST_SOURCE_NAME );
+ $this->assertTrue( $result );
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/block-bindings/wpBlockBindingsRegistry.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span></div>
</body>
</html>