[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Blocks API: WP_Block class 4 * 5 * @package WordPress 6 * @since 5.5.0 7 */ 8 9 /** 10 * Class representing a parsed instance of a block. 11 * 12 * @since 5.5.0 13 * @property array $attributes 14 */ 15 #[AllowDynamicProperties] 16 class WP_Block { 17 18 /** 19 * Original parsed array representation of block. 20 * 21 * @since 5.5.0 22 * @var array 23 */ 24 public $parsed_block; 25 26 /** 27 * Name of block. 28 * 29 * @example "core/paragraph" 30 * 31 * @since 5.5.0 32 * @var string|null 33 */ 34 public $name; 35 36 /** 37 * Block type associated with the instance. 38 * 39 * @since 5.5.0 40 * @var WP_Block_Type 41 */ 42 public $block_type; 43 44 /** 45 * Block context values. 46 * 47 * @since 5.5.0 48 * @var array 49 */ 50 public $context = array(); 51 52 /** 53 * All available context of the current hierarchy. 54 * 55 * @since 5.5.0 56 * @var array 57 */ 58 protected $available_context = array(); 59 60 /** 61 * Block type registry. 62 * 63 * @since 5.9.0 64 * @var WP_Block_Type_Registry 65 */ 66 protected $registry; 67 68 /** 69 * List of inner blocks (of this same class) 70 * 71 * @since 5.5.0 72 * @var WP_Block_List 73 */ 74 public $inner_blocks = array(); 75 76 /** 77 * Resultant HTML from inside block comment delimiters after removing inner 78 * blocks. 79 * 80 * @example "...Just <!-- wp:test /--> testing..." -> "Just testing..." 81 * 82 * @since 5.5.0 83 * @var string 84 */ 85 public $inner_html = ''; 86 87 /** 88 * List of string fragments and null markers where inner blocks were found 89 * 90 * @example array( 91 * 'inner_html' => 'BeforeInnerAfter', 92 * 'inner_blocks' => array( block, block ), 93 * 'inner_content' => array( 'Before', null, 'Inner', null, 'After' ), 94 * ) 95 * 96 * @since 5.5.0 97 * @var array 98 */ 99 public $inner_content = array(); 100 101 /** 102 * Constructor. 103 * 104 * Populates object properties from the provided block instance argument. 105 * 106 * The given array of context values will not necessarily be available on 107 * the instance itself, but is treated as the full set of values provided by 108 * the block's ancestry. This is assigned to the private `available_context` 109 * property. Only values which are configured to consumed by the block via 110 * its registered type will be assigned to the block's `context` property. 111 * 112 * @since 5.5.0 113 * 114 * @param array $block { 115 * An associative array of a single parsed block object. See WP_Block_Parser_Block. 116 * 117 * @type string|null $blockName Name of block. 118 * @type array $attrs Attributes from block comment delimiters. 119 * @type array $innerBlocks List of inner blocks. An array of arrays that 120 * have the same structure as this one. 121 * @type string $innerHTML HTML from inside block comment delimiters. 122 * @type array $innerContent List of string fragments and null markers where inner blocks were found. 123 * } 124 * @param array $available_context Optional array of ancestry context values. 125 * @param WP_Block_Type_Registry $registry Optional block type registry. 126 */ 127 public function __construct( $block, $available_context = array(), $registry = null ) { 128 $this->parsed_block = $block; 129 $this->name = $block['blockName']; 130 131 if ( is_null( $registry ) ) { 132 $registry = WP_Block_Type_Registry::get_instance(); 133 } 134 135 $this->registry = $registry; 136 137 $this->block_type = $registry->get_registered( $this->name ); 138 139 $this->available_context = $available_context; 140 141 $this->refresh_context_dependents(); 142 } 143 144 /** 145 * Updates the context for the current block and its inner blocks. 146 * 147 * The method updates the context of inner blocks, if any, by passing down 148 * any context values the block provides (`provides_context`). 149 * 150 * If the block has inner blocks, the method recursively processes them by creating new instances of `WP_Block` 151 * for each inner block and updating their context based on the block's `provides_context` property. 152 * 153 * @since 6.8.0 154 */ 155 public function refresh_context_dependents() { 156 /* 157 * Merging the `$context` property here is not ideal, but for now needs to happen because of backward compatibility. 158 * Ideally, the `$context` property itself would not be filterable directly and only the `$available_context` would be filterable. 159 * However, this needs to be separately explored whether it's possible without breakage. 160 */ 161 $this->available_context = array_merge( $this->available_context, $this->context ); 162 163 if ( ! empty( $this->block_type->uses_context ) ) { 164 foreach ( $this->block_type->uses_context as $context_name ) { 165 if ( array_key_exists( $context_name, $this->available_context ) ) { 166 $this->context[ $context_name ] = $this->available_context[ $context_name ]; 167 } 168 } 169 } 170 171 $this->refresh_parsed_block_dependents(); 172 } 173 174 /** 175 * Updates the parsed block content for the current block and its inner blocks. 176 * 177 * This method sets the `inner_html` and `inner_content` properties of the block based on the parsed 178 * block content provided during initialization. It ensures that the block instance reflects the 179 * most up-to-date content for both the inner HTML and any string fragments around inner blocks. 180 * 181 * If the block has inner blocks, this method initializes a new `WP_Block_List` for them, ensuring the 182 * correct content and context are updated for each nested block. 183 * 184 * @since 6.8.0 185 */ 186 public function refresh_parsed_block_dependents() { 187 if ( ! empty( $this->parsed_block['innerBlocks'] ) ) { 188 $child_context = $this->available_context; 189 190 if ( ! empty( $this->block_type->provides_context ) ) { 191 foreach ( $this->block_type->provides_context as $context_name => $attribute_name ) { 192 if ( array_key_exists( $attribute_name, $this->attributes ) ) { 193 $child_context[ $context_name ] = $this->attributes[ $attribute_name ]; 194 } 195 } 196 } 197 198 $this->inner_blocks = new WP_Block_List( $this->parsed_block['innerBlocks'], $child_context, $this->registry ); 199 } 200 201 if ( ! empty( $this->parsed_block['innerHTML'] ) ) { 202 $this->inner_html = $this->parsed_block['innerHTML']; 203 } 204 205 if ( ! empty( $this->parsed_block['innerContent'] ) ) { 206 $this->inner_content = $this->parsed_block['innerContent']; 207 } 208 } 209 210 /** 211 * Returns a value from an inaccessible property. 212 * 213 * This is used to lazily initialize the `attributes` property of a block, 214 * such that it is only prepared with default attributes at the time that 215 * the property is accessed. For all other inaccessible properties, a `null` 216 * value is returned. 217 * 218 * @since 5.5.0 219 * 220 * @param string $name Property name. 221 * @return array|null Prepared attributes, or null. 222 */ 223 public function __get( $name ) { 224 if ( 'attributes' === $name ) { 225 $this->attributes = isset( $this->parsed_block['attrs'] ) ? 226 $this->parsed_block['attrs'] : 227 array(); 228 229 if ( ! is_null( $this->block_type ) ) { 230 $this->attributes = $this->block_type->prepare_attributes_for_render( $this->attributes ); 231 } 232 233 return $this->attributes; 234 } 235 236 return null; 237 } 238 239 /** 240 * Processes the block bindings and updates the block attributes with the values from the sources. 241 * 242 * A block might contain bindings in its attributes. Bindings are mappings 243 * between an attribute of the block and a source. A "source" is a function 244 * registered with `register_block_bindings_source()` that defines how to 245 * retrieve a value from outside the block, e.g. from post meta. 246 * 247 * This function will process those bindings and update the block's attributes 248 * with the values coming from the bindings. 249 * 250 * ### Example 251 * 252 * The "bindings" property for an Image block might look like this: 253 * 254 * ```json 255 * { 256 * "metadata": { 257 * "bindings": { 258 * "title": { 259 * "source": "core/post-meta", 260 * "args": { "key": "text_custom_field" } 261 * }, 262 * "url": { 263 * "source": "core/post-meta", 264 * "args": { "key": "url_custom_field" } 265 * } 266 * } 267 * } 268 * } 269 * ``` 270 * 271 * The above example will replace the `title` and `url` attributes of the Image 272 * block with the values of the `text_custom_field` and `url_custom_field` post meta. 273 * 274 * @since 6.5.0 275 * @since 6.6.0 Handle the `__default` attribute for pattern overrides. 276 * @since 6.7.0 Return any updated bindings metadata in the computed attributes. 277 * 278 * @return array The computed block attributes for the provided block bindings. 279 */ 280 private function process_block_bindings() { 281 $block_type = $this->name; 282 $parsed_block = $this->parsed_block; 283 $computed_attributes = array(); 284 $supported_block_attributes = get_block_bindings_supported_attributes( $block_type ); 285 286 // If the block doesn't have the bindings property, isn't one of the supported 287 // block types, or the bindings property is not an array, return the block content. 288 if ( 289 empty( $supported_block_attributes ) || 290 empty( $parsed_block['attrs']['metadata']['bindings'] ) || 291 ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) 292 ) { 293 return $computed_attributes; 294 } 295 296 $bindings = $parsed_block['attrs']['metadata']['bindings']; 297 298 /* 299 * If the default binding is set for pattern overrides, replace it 300 * with a pattern override binding for all supported attributes. 301 */ 302 if ( 303 isset( $bindings['__default']['source'] ) && 304 'core/pattern-overrides' === $bindings['__default']['source'] 305 ) { 306 $updated_bindings = array(); 307 308 /* 309 * Build a binding array of all supported attributes. 310 * Note that this also omits the `__default` attribute from the 311 * resulting array. 312 */ 313 foreach ( $supported_block_attributes as $attribute_name ) { 314 // Retain any non-pattern override bindings that might be present. 315 $updated_bindings[ $attribute_name ] = isset( $bindings[ $attribute_name ] ) 316 ? $bindings[ $attribute_name ] 317 : array( 'source' => 'core/pattern-overrides' ); 318 } 319 $bindings = $updated_bindings; 320 /* 321 * Update the bindings metadata of the computed attributes. 322 * This ensures the block receives the expanded __default binding metadata when it renders. 323 */ 324 $computed_attributes['metadata'] = array_merge( 325 $parsed_block['attrs']['metadata'], 326 array( 'bindings' => $bindings ) 327 ); 328 } 329 330 foreach ( $bindings as $attribute_name => $block_binding ) { 331 // If the attribute is not in the supported list, process next attribute. 332 if ( ! in_array( $attribute_name, $supported_block_attributes, true ) ) { 333 continue; 334 } 335 // If no source is provided, or that source is not registered, process next attribute. 336 if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) { 337 continue; 338 } 339 340 $block_binding_source = get_block_bindings_source( $block_binding['source'] ); 341 if ( null === $block_binding_source ) { 342 continue; 343 } 344 345 // Adds the necessary context defined by the source. 346 if ( ! empty( $block_binding_source->uses_context ) ) { 347 foreach ( $block_binding_source->uses_context as $context_name ) { 348 if ( array_key_exists( $context_name, $this->available_context ) ) { 349 $this->context[ $context_name ] = $this->available_context[ $context_name ]; 350 } 351 } 352 } 353 354 $source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array(); 355 $source_value = $block_binding_source->get_value( $source_args, $this, $attribute_name ); 356 357 // If the value is not null, process the HTML based on the block and the attribute. 358 if ( ! is_null( $source_value ) ) { 359 $computed_attributes[ $attribute_name ] = $source_value; 360 } 361 } 362 363 return $computed_attributes; 364 } 365 366 /** 367 * Depending on the block attribute name, replace its value in the HTML based on the value provided. 368 * 369 * @since 6.5.0 370 * 371 * @param string $block_content Block content. 372 * @param string $attribute_name The attribute name to replace. 373 * @param mixed $source_value The value used to replace in the HTML. 374 * @return string The modified block content. 375 */ 376 private function replace_html( string $block_content, string $attribute_name, $source_value ) { 377 $block_type = $this->block_type; 378 if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) { 379 return $block_content; 380 } 381 382 // Depending on the attribute source, the processing will be different. 383 switch ( $block_type->attributes[ $attribute_name ]['source'] ) { 384 case 'html': 385 case 'rich-text': 386 $block_reader = self::get_block_bindings_processor( $block_content ); 387 388 // TODO: Support for CSS selectors whenever they are ready in the HTML API. 389 // In the meantime, support comma-separated selectors by exploding them into an array. 390 $selectors = explode( ',', $block_type->attributes[ $attribute_name ]['selector'] ); 391 // Add a bookmark to the first tag to be able to iterate over the selectors. 392 $block_reader->next_tag(); 393 $block_reader->set_bookmark( 'iterate-selectors' ); 394 395 foreach ( $selectors as $selector ) { 396 // If the parent tag, or any of its children, matches the selector, replace the HTML. 397 if ( strcasecmp( $block_reader->get_tag(), $selector ) === 0 || $block_reader->next_tag( 398 array( 399 'tag_name' => $selector, 400 ) 401 ) ) { 402 // TODO: Use `WP_HTML_Processor::set_inner_html` method once it's available. 403 $block_reader->release_bookmark( 'iterate-selectors' ); 404 $block_reader->replace_rich_text( wp_kses_post( $source_value ) ); 405 return $block_reader->get_updated_html(); 406 } else { 407 $block_reader->seek( 'iterate-selectors' ); 408 } 409 } 410 $block_reader->release_bookmark( 'iterate-selectors' ); 411 return $block_content; 412 413 case 'attribute': 414 $amended_content = new WP_HTML_Tag_Processor( $block_content ); 415 if ( ! $amended_content->next_tag( 416 array( 417 // TODO: build the query from CSS selector. 418 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], 419 ) 420 ) ) { 421 return $block_content; 422 } 423 $amended_content->set_attribute( $block_type->attributes[ $attribute_name ]['attribute'], $source_value ); 424 return $amended_content->get_updated_html(); 425 426 default: 427 return $block_content; 428 } 429 } 430 431 private static function get_block_bindings_processor( string $block_content ) { 432 $internal_processor_class = new class('', WP_HTML_Processor::CONSTRUCTOR_UNLOCK_CODE) extends WP_HTML_Processor { 433 /** 434 * Replace the rich text content between a tag opener and matching closer. 435 * 436 * When stopped on a tag opener, replace the content enclosed by it and its 437 * matching closer with the provided rich text. 438 * 439 * @param string $rich_text The rich text to replace the original content with. 440 * @return bool True on success. 441 */ 442 public function replace_rich_text( $rich_text ) { 443 if ( $this->is_tag_closer() || ! $this->expects_closer() ) { 444 return false; 445 } 446 447 $depth = $this->get_current_depth(); 448 449 $this->set_bookmark( '_wp_block_bindings_tag_opener' ); 450 // The bookmark names are prefixed with `_` so the key below has an extra `_`. 451 $tag_opener = $this->bookmarks['__wp_block_bindings_tag_opener']; 452 $start = $tag_opener->start + $tag_opener->length; 453 $this->release_bookmark( '_wp_block_bindings_tag_opener' ); 454 455 // Find matching tag closer. 456 while ( $this->next_token() && $this->get_current_depth() >= $depth ) { 457 } 458 459 $this->set_bookmark( '_wp_block_bindings_tag_closer' ); 460 $tag_closer = $this->bookmarks['__wp_block_bindings_tag_closer']; 461 $end = $tag_closer->start; 462 $this->release_bookmark( '_wp_block_bindings_tag_closer' ); 463 464 $this->lexical_updates[] = new WP_HTML_Text_Replacement( 465 $start, 466 $end - $start, 467 $rich_text 468 ); 469 470 return true; 471 } 472 }; 473 474 return $internal_processor_class::create_fragment( $block_content ); 475 } 476 477 /** 478 * Generates the render output for the block. 479 * 480 * @since 5.5.0 481 * @since 6.5.0 Added block bindings processing. 482 * 483 * @global WP_Post $post Global post object. 484 * 485 * @param array $options { 486 * Optional options object. 487 * 488 * @type bool $dynamic Defaults to 'true'. Optionally set to false to avoid using the block's render_callback. 489 * } 490 * @return string Rendered block output. 491 */ 492 public function render( $options = array() ) { 493 global $post; 494 495 /* 496 * There can be only one root interactive block at a time because the rendered HTML of that block contains 497 * the rendered HTML of all its inner blocks, including any interactive block. 498 */ 499 static $root_interactive_block = null; 500 /** 501 * Filters whether Interactivity API should process directives. 502 * 503 * @since 6.6.0 504 * 505 * @param bool $enabled Whether the directives processing is enabled. 506 */ 507 $interactivity_process_directives_enabled = apply_filters( 'interactivity_process_directives', true ); 508 if ( 509 $interactivity_process_directives_enabled && null === $root_interactive_block && ( 510 ( isset( $this->block_type->supports['interactivity'] ) && true === $this->block_type->supports['interactivity'] ) || 511 ! empty( $this->block_type->supports['interactivity']['interactive'] ) 512 ) 513 ) { 514 $root_interactive_block = $this; 515 } 516 517 $options = wp_parse_args( 518 $options, 519 array( 520 'dynamic' => true, 521 ) 522 ); 523 524 // Process the block bindings and get attributes updated with the values from the sources. 525 $computed_attributes = $this->process_block_bindings(); 526 if ( ! empty( $computed_attributes ) ) { 527 // Merge the computed attributes with the original attributes. 528 $this->attributes = array_merge( $this->attributes, $computed_attributes ); 529 } 530 531 $is_dynamic = $options['dynamic'] && $this->name && null !== $this->block_type && $this->block_type->is_dynamic(); 532 $block_content = ''; 533 534 if ( ! $options['dynamic'] || empty( $this->block_type->skip_inner_blocks ) ) { 535 $index = 0; 536 537 foreach ( $this->inner_content as $chunk ) { 538 if ( is_string( $chunk ) ) { 539 $block_content .= $chunk; 540 } else { 541 $inner_block = $this->inner_blocks[ $index ]; 542 $parent_block = $this; 543 544 /** This filter is documented in wp-includes/blocks.php */ 545 $pre_render = apply_filters( 'pre_render_block', null, $inner_block->parsed_block, $parent_block ); 546 547 if ( ! is_null( $pre_render ) ) { 548 $block_content .= $pre_render; 549 } else { 550 $source_block = $inner_block->parsed_block; 551 $inner_block_context = $inner_block->context; 552 553 /** This filter is documented in wp-includes/blocks.php */ 554 $inner_block->parsed_block = apply_filters( 'render_block_data', $inner_block->parsed_block, $source_block, $parent_block ); 555 556 /** This filter is documented in wp-includes/blocks.php */ 557 $inner_block->context = apply_filters( 'render_block_context', $inner_block->context, $inner_block->parsed_block, $parent_block ); 558 559 /* 560 * The `refresh_context_dependents()` method already calls `refresh_parsed_block_dependents()`. 561 * Therefore the second condition is irrelevant if the first one is satisfied. 562 */ 563 if ( $inner_block->context !== $inner_block_context ) { 564 $inner_block->refresh_context_dependents(); 565 } elseif ( $inner_block->parsed_block !== $source_block ) { 566 $inner_block->refresh_parsed_block_dependents(); 567 } 568 569 $block_content .= $inner_block->render(); 570 } 571 572 ++$index; 573 } 574 } 575 } 576 577 if ( ! empty( $computed_attributes ) && ! empty( $block_content ) ) { 578 foreach ( $computed_attributes as $attribute_name => $source_value ) { 579 $block_content = $this->replace_html( $block_content, $attribute_name, $source_value ); 580 } 581 } 582 583 if ( $is_dynamic ) { 584 $global_post = $post; 585 $parent = WP_Block_Supports::$block_to_render; 586 587 WP_Block_Supports::$block_to_render = $this->parsed_block; 588 589 $block_content = (string) call_user_func( $this->block_type->render_callback, $this->attributes, $block_content, $this ); 590 591 WP_Block_Supports::$block_to_render = $parent; 592 593 $post = $global_post; 594 } 595 596 if ( ( ! empty( $this->block_type->script_handles ) ) ) { 597 foreach ( $this->block_type->script_handles as $script_handle ) { 598 wp_enqueue_script( $script_handle ); 599 } 600 } 601 602 if ( ! empty( $this->block_type->view_script_handles ) ) { 603 foreach ( $this->block_type->view_script_handles as $view_script_handle ) { 604 wp_enqueue_script( $view_script_handle ); 605 } 606 } 607 608 if ( ! empty( $this->block_type->view_script_module_ids ) ) { 609 foreach ( $this->block_type->view_script_module_ids as $view_script_module_id ) { 610 wp_enqueue_script_module( $view_script_module_id ); 611 } 612 } 613 614 /* 615 * For Core blocks, these styles are only enqueued if `wp_should_load_separate_core_block_assets()` returns 616 * true. Otherwise these `wp_enqueue_style()` calls will not have any effect, as the Core blocks are relying on 617 * the combined 'wp-block-library' stylesheet instead, which is unconditionally enqueued. 618 */ 619 if ( ( ! empty( $this->block_type->style_handles ) ) ) { 620 foreach ( $this->block_type->style_handles as $style_handle ) { 621 wp_enqueue_style( $style_handle ); 622 } 623 } 624 625 if ( ( ! empty( $this->block_type->view_style_handles ) ) ) { 626 foreach ( $this->block_type->view_style_handles as $view_style_handle ) { 627 wp_enqueue_style( $view_style_handle ); 628 } 629 } 630 631 /** 632 * Filters the content of a single block. 633 * 634 * @since 5.0.0 635 * @since 5.9.0 The `$instance` parameter was added. 636 * 637 * @param string $block_content The block content. 638 * @param array $block The full block, including name and attributes. 639 * @param WP_Block $instance The block instance. 640 */ 641 $block_content = apply_filters( 'render_block', $block_content, $this->parsed_block, $this ); 642 643 /** 644 * Filters the content of a single block. 645 * 646 * The dynamic portion of the hook name, `$name`, refers to 647 * the block name, e.g. "core/paragraph". 648 * 649 * @since 5.7.0 650 * @since 5.9.0 The `$instance` parameter was added. 651 * 652 * @param string $block_content The block content. 653 * @param array $block The full block, including name and attributes. 654 * @param WP_Block $instance The block instance. 655 */ 656 $block_content = apply_filters( "render_block_{$this->name}", $block_content, $this->parsed_block, $this ); 657 658 if ( $root_interactive_block === $this ) { 659 // The root interactive block has finished rendering. Time to process directives. 660 $block_content = wp_interactivity_process_directives( $block_content ); 661 $root_interactive_block = null; 662 } 663 664 return $block_content; 665 } 666 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Fri Oct 10 08:20:03 2025 | Cross-referenced by PHPXref |