wp/wp-includes/class-wp-block-bindings-registry.php
changeset 21 48c4eec2b7e6
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     1 <?php
       
     2 /**
       
     3  * Block Bindings API: WP_Block_Bindings_Registry class.
       
     4  *
       
     5  * Supports overriding content in blocks by connecting them to different sources.
       
     6  *
       
     7  * @package WordPress
       
     8  * @subpackage Block Bindings
       
     9  * @since 6.5.0
       
    10  */
       
    11 
       
    12 /**
       
    13  * Core class used for interacting with block bindings sources.
       
    14  *
       
    15  * @since 6.5.0
       
    16  */
       
    17 final class WP_Block_Bindings_Registry {
       
    18 
       
    19 	/**
       
    20 	 * Holds the registered block bindings sources, keyed by source identifier.
       
    21 	 *
       
    22 	 * @since 6.5.0
       
    23 	 * @var WP_Block_Bindings_Source[]
       
    24 	 */
       
    25 	private $sources = array();
       
    26 
       
    27 	/**
       
    28 	 * Container for the main instance of the class.
       
    29 	 *
       
    30 	 * @since 6.5.0
       
    31 	 * @var WP_Block_Bindings_Registry|null
       
    32 	 */
       
    33 	private static $instance = null;
       
    34 
       
    35 	/**
       
    36 	 * Supported source properties that can be passed to the registered source.
       
    37 	 *
       
    38 	 * @since 6.5.0
       
    39 	 * @var string[]
       
    40 	 */
       
    41 	private $allowed_source_properties = array(
       
    42 		'label',
       
    43 		'get_value_callback',
       
    44 		'uses_context',
       
    45 	);
       
    46 
       
    47 	/**
       
    48 	 * Supported blocks that can use the block bindings API.
       
    49 	 *
       
    50 	 * @since 6.5.0
       
    51 	 * @var string[]
       
    52 	 */
       
    53 	private $supported_blocks = array(
       
    54 		'core/paragraph',
       
    55 		'core/heading',
       
    56 		'core/image',
       
    57 		'core/button',
       
    58 	);
       
    59 
       
    60 	/**
       
    61 	 * Registers a new block bindings source.
       
    62 	 *
       
    63 	 * This is a low-level method. For most use cases, it is recommended to use
       
    64 	 * the `register_block_bindings_source()` function instead.
       
    65 	 *
       
    66 	 * @see register_block_bindings_source()
       
    67 	 *
       
    68 	 * Sources are used to override block's original attributes with a value
       
    69 	 * coming from the source. Once a source is registered, it can be used by a
       
    70 	 * block by setting its `metadata.bindings` attribute to a value that refers
       
    71 	 * to the source.
       
    72 	 *
       
    73 	 * @since 6.5.0
       
    74 	 *
       
    75 	 * @param string $source_name       The name of the source. It must be a string containing a namespace prefix, i.e.
       
    76 	 *                                  `my-plugin/my-custom-source`. It must only contain lowercase alphanumeric
       
    77 	 *                                  characters, the forward slash `/` and dashes.
       
    78 	 * @param array  $source_properties {
       
    79 	 *     The array of arguments that are used to register a source.
       
    80 	 *
       
    81 	 *     @type string   $label              The label of the source.
       
    82 	 *     @type callable $get_value_callback A callback executed when the source is processed during block rendering.
       
    83 	 *                                        The callback should have the following signature:
       
    84 	 *
       
    85 	 *                                        `function( $source_args, $block_instance, $attribute_name ): mixed`
       
    86 	 *                                            - @param array    $source_args    Array containing source arguments
       
    87 	 *                                                                              used to look up the override value,
       
    88 	 *                                                                              i.e. {"key": "foo"}.
       
    89 	 *                                            - @param WP_Block $block_instance The block instance.
       
    90 	 *                                            - @param string   $attribute_name The name of the target attribute.
       
    91 	 *                                        The callback has a mixed return type; it may return a string to override
       
    92 	 *                                        the block's original value, null, false to remove an attribute, etc.
       
    93 	 *     @type string[] $uses_context       Optional. Array of values to add to block `uses_context` needed by the source.
       
    94 	 * }
       
    95 	 * @return WP_Block_Bindings_Source|false Source when the registration was successful, or `false` on failure.
       
    96 	 */
       
    97 	public function register( string $source_name, array $source_properties ) {
       
    98 		if ( ! is_string( $source_name ) ) {
       
    99 			_doing_it_wrong(
       
   100 				__METHOD__,
       
   101 				__( 'Block bindings source name must be a string.' ),
       
   102 				'6.5.0'
       
   103 			);
       
   104 			return false;
       
   105 		}
       
   106 
       
   107 		if ( preg_match( '/[A-Z]+/', $source_name ) ) {
       
   108 			_doing_it_wrong(
       
   109 				__METHOD__,
       
   110 				__( 'Block bindings source names must not contain uppercase characters.' ),
       
   111 				'6.5.0'
       
   112 			);
       
   113 			return false;
       
   114 		}
       
   115 
       
   116 		$name_matcher = '/^[a-z0-9-]+\/[a-z0-9-]+$/';
       
   117 		if ( ! preg_match( $name_matcher, $source_name ) ) {
       
   118 			_doing_it_wrong(
       
   119 				__METHOD__,
       
   120 				__( 'Block bindings source names must contain a namespace prefix. Example: my-plugin/my-custom-source' ),
       
   121 				'6.5.0'
       
   122 			);
       
   123 			return false;
       
   124 		}
       
   125 
       
   126 		if ( $this->is_registered( $source_name ) ) {
       
   127 			_doing_it_wrong(
       
   128 				__METHOD__,
       
   129 				/* translators: %s: Block bindings source name. */
       
   130 				sprintf( __( 'Block bindings source "%s" already registered.' ), $source_name ),
       
   131 				'6.5.0'
       
   132 			);
       
   133 			return false;
       
   134 		}
       
   135 
       
   136 		// Validates that the source properties contain the label.
       
   137 		if ( ! isset( $source_properties['label'] ) ) {
       
   138 			_doing_it_wrong(
       
   139 				__METHOD__,
       
   140 				__( 'The $source_properties must contain a "label".' ),
       
   141 				'6.5.0'
       
   142 			);
       
   143 			return false;
       
   144 		}
       
   145 
       
   146 		// Validates that the source properties contain the get_value_callback.
       
   147 		if ( ! isset( $source_properties['get_value_callback'] ) ) {
       
   148 			_doing_it_wrong(
       
   149 				__METHOD__,
       
   150 				__( 'The $source_properties must contain a "get_value_callback".' ),
       
   151 				'6.5.0'
       
   152 			);
       
   153 			return false;
       
   154 		}
       
   155 
       
   156 		// Validates that the get_value_callback is a valid callback.
       
   157 		if ( ! is_callable( $source_properties['get_value_callback'] ) ) {
       
   158 			_doing_it_wrong(
       
   159 				__METHOD__,
       
   160 				__( 'The "get_value_callback" parameter must be a valid callback.' ),
       
   161 				'6.5.0'
       
   162 			);
       
   163 			return false;
       
   164 		}
       
   165 
       
   166 		// Validates that the uses_context parameter is an array.
       
   167 		if ( isset( $source_properties['uses_context'] ) && ! is_array( $source_properties['uses_context'] ) ) {
       
   168 			_doing_it_wrong(
       
   169 				__METHOD__,
       
   170 				__( 'The "uses_context" parameter must be an array.' ),
       
   171 				'6.5.0'
       
   172 			);
       
   173 			return false;
       
   174 		}
       
   175 
       
   176 		if ( ! empty( array_diff( array_keys( $source_properties ), $this->allowed_source_properties ) ) ) {
       
   177 			_doing_it_wrong(
       
   178 				__METHOD__,
       
   179 				__( 'The $source_properties array contains invalid properties.' ),
       
   180 				'6.5.0'
       
   181 			);
       
   182 			return false;
       
   183 		}
       
   184 
       
   185 		$source = new WP_Block_Bindings_Source(
       
   186 			$source_name,
       
   187 			$source_properties
       
   188 		);
       
   189 
       
   190 		$this->sources[ $source_name ] = $source;
       
   191 
       
   192 		// Adds `uses_context` defined by block bindings sources.
       
   193 		add_filter(
       
   194 			'get_block_type_uses_context',
       
   195 			function ( $uses_context, $block_type ) use ( $source ) {
       
   196 				if ( ! in_array( $block_type->name, $this->supported_blocks, true ) || empty( $source->uses_context ) ) {
       
   197 					return $uses_context;
       
   198 				}
       
   199 				// Use array_values to reset the array keys.
       
   200 				return array_values( array_unique( array_merge( $uses_context, $source->uses_context ) ) );
       
   201 			},
       
   202 			10,
       
   203 			2
       
   204 		);
       
   205 
       
   206 		return $source;
       
   207 	}
       
   208 
       
   209 	/**
       
   210 	 * Unregisters a block bindings source.
       
   211 	 *
       
   212 	 * @since 6.5.0
       
   213 	 *
       
   214 	 * @param string $source_name Block bindings source name including namespace.
       
   215 	 * @return WP_Block_Bindings_Source|false The unregistered block bindings source on success and `false` otherwise.
       
   216 	 */
       
   217 	public function unregister( string $source_name ) {
       
   218 		if ( ! $this->is_registered( $source_name ) ) {
       
   219 			_doing_it_wrong(
       
   220 				__METHOD__,
       
   221 				/* translators: %s: Block bindings source name. */
       
   222 				sprintf( __( 'Block binding "%s" not found.' ), $source_name ),
       
   223 				'6.5.0'
       
   224 			);
       
   225 			return false;
       
   226 		}
       
   227 
       
   228 		$unregistered_source = $this->sources[ $source_name ];
       
   229 		unset( $this->sources[ $source_name ] );
       
   230 
       
   231 		return $unregistered_source;
       
   232 	}
       
   233 
       
   234 	/**
       
   235 	 * Retrieves the list of all registered block bindings sources.
       
   236 	 *
       
   237 	 * @since 6.5.0
       
   238 	 *
       
   239 	 * @return WP_Block_Bindings_Source[] The array of registered sources.
       
   240 	 */
       
   241 	public function get_all_registered() {
       
   242 		return $this->sources;
       
   243 	}
       
   244 
       
   245 	/**
       
   246 	 * Retrieves a registered block bindings source.
       
   247 	 *
       
   248 	 * @since 6.5.0
       
   249 	 *
       
   250 	 * @param string $source_name The name of the source.
       
   251 	 * @return WP_Block_Bindings_Source|null The registered block bindings source, or `null` if it is not registered.
       
   252 	 */
       
   253 	public function get_registered( string $source_name ) {
       
   254 		if ( ! $this->is_registered( $source_name ) ) {
       
   255 			return null;
       
   256 		}
       
   257 
       
   258 		return $this->sources[ $source_name ];
       
   259 	}
       
   260 
       
   261 	/**
       
   262 	 * Checks if a block bindings source is registered.
       
   263 	 *
       
   264 	 * @since 6.5.0
       
   265 	 *
       
   266 	 * @param string $source_name The name of the source.
       
   267 	 * @return bool `true` if the block bindings source is registered, `false` otherwise.
       
   268 	 */
       
   269 	public function is_registered( $source_name ) {
       
   270 		return isset( $this->sources[ $source_name ] );
       
   271 	}
       
   272 
       
   273 	/**
       
   274 	 * Wakeup magic method.
       
   275 	 *
       
   276 	 * @since 6.5.0
       
   277 	 */
       
   278 	public function __wakeup() {
       
   279 		if ( ! $this->sources ) {
       
   280 			return;
       
   281 		}
       
   282 		if ( ! is_array( $this->sources ) ) {
       
   283 			throw new UnexpectedValueException();
       
   284 		}
       
   285 		foreach ( $this->sources as $value ) {
       
   286 			if ( ! $value instanceof WP_Block_Bindings_Source ) {
       
   287 				throw new UnexpectedValueException();
       
   288 			}
       
   289 		}
       
   290 	}
       
   291 
       
   292 	/**
       
   293 	 * Utility method to retrieve the main instance of the class.
       
   294 	 *
       
   295 	 * The instance will be created if it does not exist yet.
       
   296 	 *
       
   297 	 * @since 6.5.0
       
   298 	 *
       
   299 	 * @return WP_Block_Bindings_Registry The main instance.
       
   300 	 */
       
   301 	public static function get_instance() {
       
   302 		if ( null === self::$instance ) {
       
   303 			self::$instance = new self();
       
   304 		}
       
   305 
       
   306 		return self::$instance;
       
   307 	}
       
   308 }