| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * WP_View_Config_Data class 4 * 5 * @package WordPress 6 * @since 7.1.0 7 */ 8 9 /** 10 * Holds an entity's view configuration while it is being built. 11 * 12 * An instance of this class is what `get_entity_view_config_{$kind}_{$name}` 13 * filter callbacks receive: a callback changes the configuration by calling 14 * methods on the instance and returning it. The configuration has four 15 * top-level keys — `default_view`, `default_layouts`, `view_list`, and 16 * `form` — and there are two ways to contribute: 17 * 18 * - The `update_*()` methods merge partial changes (patches) into what is 19 * already there, each covering one part of the configuration: 20 * `update_properties()` for `default_view`, `default_layouts`, and the 21 * `form` settings other than its `fields`; `update_view_list_items()` for 22 * the `view_list` entries, keyed by view `slug`; and `update_form_fields()` 23 * for the `form` fields, keyed by field `id`. This is what plugins should 24 * use: patches compose with core's configuration and with other plugins'. 25 * - `set()` replaces a whole top-level key. It shouldn't be the default 26 * choice — a callback using it stops inheriting core's future changes to 27 * that key — but it's useful for cases like a post type that doesn't 28 * want the default form at all. 29 * 30 * Patches follow three shared rules: an associative array merges key by 31 * key, a numerically indexed array replaces the current value wholesale, 32 * and `null` deletes what it names — deleting a whole top-level key resets 33 * it to its default. Each patch and each `set()` value also declares the 34 * configuration schema version it was written against (currently 1), so a 35 * future WordPress release that changes the configuration shape can migrate 36 * existing patches forward instead of breaking them. 37 * 38 * @since 7.1.0 39 */ 40 class WP_View_Config_Data { 41 42 /** 43 * The latest supported configuration schema version. 44 * 45 * @since 7.1.0 46 * @var int 47 */ 48 const LATEST_VERSION = 1; 49 50 /** 51 * The documented top-level configuration keys. 52 * 53 * @since 7.1.0 54 * @var string[] 55 */ 56 const CONFIG_KEYS = array( 'default_view', 'default_layouts', 'view_list', 'form' ); 57 58 /** 59 * The configuration being contributed to. 60 * 61 * @since 7.1.0 62 * @var array 63 */ 64 private $config; 65 66 /** 67 * Constructor. 68 * 69 * @since 7.1.0 70 * 71 * @param array $config The base configuration to contribute to. 72 */ 73 public function __construct( array $config ) { 74 $this->config = $config; 75 } 76 77 /** 78 * Returns the current configuration array. 79 * 80 * @since 7.1.0 81 * 82 * @return array The configuration. 83 */ 84 public function get_config() { 85 return $this->config; 86 } 87 88 /** 89 * Replaces a whole top-level key with a new value. 90 * 91 * It shouldn't be the default choice — a callback using it stops 92 * inheriting core's future changes to that key — but it's useful for 93 * cases like a post type that doesn't want the default form at all. 94 * 95 * A value that declares an unsupported schema version is rejected and 96 * does not replace anything. 97 * 98 * @since 7.1.0 99 * 100 * @param string $key The configuration key to replace. 101 * @param mixed $value The new value. 102 * @param int $version The schema version the value was authored against. 103 * @return WP_View_Config_Data The instance, for chaining. 104 */ 105 public function set( $key, $value, int $version ) { 106 if ( ! $this->check_version( $version, __METHOD__ ) ) { 107 return $this; 108 } 109 110 if ( ! in_array( $key, self::CONFIG_KEYS, true ) ) { 111 _doing_it_wrong( 112 __METHOD__, 113 sprintf( 114 /* translators: %s: the configuration key. */ 115 esc_html__( '"%s" is not a documented view configuration key.' ), 116 esc_html( $key ) 117 ), 118 '7.1.0' 119 ); 120 return $this; 121 } 122 123 $this->config[ $key ] = $value; 124 return $this; 125 } 126 127 /** 128 * Merges a partial configuration into `default_view`, `default_layouts`, 129 * and the `form` settings other than its `fields`. 130 * 131 * An associative array merges key by key, a numerically indexed array 132 * replaces the current value wholesale, and `null` deletes the key it 133 * names; deleting a whole top-level key (any documented key, including 134 * `view_list`) resets it to its default. 135 * 136 * The keyed collections have dedicated methods and are rejected here: a 137 * non-null `view_list` value must go through `update_view_list_items()`, 138 * and a `fields` key inside a `form` value must go through 139 * `update_form_fields()`. 140 * 141 * A patch that declares an unsupported schema version is rejected and 142 * does not merge. 143 * 144 * @since 7.1.0 145 * 146 * @param array $patch The partial configuration to merge. 147 * @param int $version The schema version the patch was authored against. 148 * @return WP_View_Config_Data The instance, for chaining. 149 */ 150 public function update_properties( array $patch, int $version ) { 151 if ( ! $this->check_version( $version, __METHOD__ ) ) { 152 return $this; 153 } 154 155 foreach ( $patch as $key => $value ) { 156 if ( ! in_array( $key, self::CONFIG_KEYS, true ) ) { 157 _doing_it_wrong( 158 __METHOD__, 159 sprintf( 160 /* translators: %s: the configuration key. */ 161 esc_html__( '"%s" is not a documented view configuration key.' ), 162 esc_html( $key ) 163 ), 164 '7.1.0' 165 ); 166 continue; 167 } 168 // A null patch value drops the whole key from the container rather 169 // than assigning null. 170 if ( null === $value ) { 171 unset( $this->config[ $key ] ); 172 continue; 173 } 174 if ( 'view_list' === $key ) { 175 _doing_it_wrong( 176 __METHOD__, 177 esc_html__( 'The "view_list" entries are patched by identity. Use update_view_list_items() instead.' ), 178 '7.1.0' 179 ); 180 continue; 181 } 182 if ( 'form' === $key ) { 183 $value = $this->extract_form_properties( $value ); 184 // Nothing left to merge: the value was off-shape, or held only 185 // the rejected `fields` key. 186 if ( null === $value || array() === $value ) { 187 continue; 188 } 189 } 190 $this->config[ $key ] = $this->deep_merge( $this->config[ $key ] ?? array(), $value ); 191 } 192 193 return $this; 194 } 195 196 /** 197 * Adds, updates, or removes `view_list` entries, keyed by view `slug`. 198 * 199 * Each patch key names the `slug` of the view it targets: a matching view 200 * merges in place and keeps its position (following the shared rules — 201 * e.g. the view's `filters`, being numerically indexed, replace 202 * wholesale), an unknown slug appends a new view to the end, and `null` 203 * removes the view. The patch key is the identity: a `slug` property 204 * inside the value is ignored. A `null` for a slug that is not found is a 205 * silent no-op — the view may have been removed by another callback or 206 * simply not apply to this entity. 207 * 208 * A patch that declares an unsupported schema version is rejected and 209 * does not merge. 210 * 211 * @since 7.1.0 212 * 213 * @param array $items The view patches, keyed by slug. 214 * @param int $version The schema version the patch was authored against. 215 * @return WP_View_Config_Data The instance, for chaining. 216 */ 217 public function update_view_list_items( array $items, int $version ) { 218 if ( ! $this->check_version( $version, __METHOD__ ) ) { 219 return $this; 220 } 221 222 if ( empty( $items ) ) { 223 return $this; 224 } 225 if ( array_is_list( $items ) ) { 226 _doing_it_wrong( 227 __METHOD__, 228 esc_html__( 'A view list patch must be keyed by view "slug".' ), 229 '7.1.0' 230 ); 231 return $this; 232 } 233 234 $view_list = isset( $this->config['view_list'] ) && is_array( $this->config['view_list'] ) ? $this->config['view_list'] : array(); 235 236 foreach ( $items as $slug => $value ) { 237 // PHP casts numeric-string array keys to integers; identities are strings. 238 $slug = (string) $slug; 239 240 if ( null === $value ) { 241 $view_list = array_values( 242 array_filter( 243 $view_list, 244 static fn( $item ) => ! is_array( $item ) || ! isset( $item['slug'] ) || $item['slug'] !== $slug 245 ) 246 ); 247 continue; 248 } 249 250 if ( ! is_array( $value ) || ( array() !== $value && array_is_list( $value ) ) ) { 251 _doing_it_wrong( 252 __METHOD__, 253 esc_html__( 'Each view patch must be an associative array of view properties, or null to remove the view.' ), 254 '7.1.0' 255 ); 256 continue; 257 } 258 259 // The patch key is the identity. 260 unset( $value['slug'] ); 261 262 $index = null; 263 foreach ( $view_list as $i => $item ) { 264 if ( is_array( $item ) && isset( $item['slug'] ) && $item['slug'] === $slug ) { 265 $index = $i; 266 break; 267 } 268 } 269 270 if ( null === $index ) { 271 $view_list[] = array_merge( array( 'slug' => $slug ), $value ); 272 continue; 273 } 274 // An empty patch value has nothing to merge (and deep_merge would 275 // treat an empty array as a list, replacing the whole view). 276 if ( array() !== $value ) { 277 $view_list[ $index ] = $this->deep_merge( $view_list[ $index ], $value ); 278 } 279 } 280 281 $this->config['view_list'] = array_values( $view_list ); 282 283 return $this; 284 } 285 286 /** 287 * Adds, updates, or removes `form` fields, keyed by field `id`. 288 * 289 * Each patch key names the `id` of the field it targets, and the field is 290 * found wherever it lives — at the top level or nested inside a group's 291 * `children`. Fields are visited in document order and a group is checked 292 * before its own children, so when an id appears at both levels the group 293 * wins. A matching field merges in place, an unknown id appends a new field 294 * to the end of the top-level fields, and `null` removes the field. The 295 * patch key is the identity: an `id` property inside the value is ignored. 296 * A `null` for an id that is not found is a silent no-op — the field may 297 * have been removed by another callback or simply not apply to this 298 * entity. 299 * 300 * Inside a field patch, `children` follows the shared rules: an associative 301 * array merges into the group's children by id (appending unknown ones), a 302 * numerically indexed array replaces the children wholesale, and `null` 303 * deletes the key. 304 * 305 * A patch that declares an unsupported schema version is rejected and 306 * does not merge. 307 * 308 * @since 7.1.0 309 * 310 * @param array $fields The field patches, keyed by field id. 311 * @param int $version The schema version the patch was authored against. 312 * @return WP_View_Config_Data The instance, for chaining. 313 */ 314 public function update_form_fields( array $fields, int $version ) { 315 if ( ! $this->check_version( $version, __METHOD__ ) ) { 316 return $this; 317 } 318 319 if ( empty( $fields ) ) { 320 return $this; 321 } 322 if ( array_is_list( $fields ) ) { 323 _doing_it_wrong( 324 __METHOD__, 325 esc_html__( 'A fields patch must be keyed by field "id".' ), 326 '7.1.0' 327 ); 328 return $this; 329 } 330 331 if ( ! isset( $this->config['form'] ) || ! is_array( $this->config['form'] ) ) { 332 $this->config['form'] = array(); 333 } 334 $current = isset( $this->config['form']['fields'] ) && is_array( $this->config['form']['fields'] ) ? $this->config['form']['fields'] : array(); 335 336 $this->config['form']['fields'] = $this->merge_fields_by_identity( $current, $fields ); 337 338 return $this; 339 } 340 341 /** 342 * Validates a declared patch version, reporting misuse against the given 343 * public method. 344 * 345 * @since 7.1.0 346 * 347 * @param int $version The declared version. 348 * @param string $method The public method the patch was passed to. 349 * @return bool Whether the declared version is a supported schema version. 350 */ 351 private function check_version( int $version, $method ) { 352 if ( $version >= 1 && $version <= self::LATEST_VERSION ) { 353 return true; 354 } 355 356 _doing_it_wrong( 357 esc_html( $method ), 358 esc_html__( 'A view configuration contribution must declare a supported schema version.' ), 359 '7.1.0' 360 ); 361 362 return false; 363 } 364 365 /** 366 * Validates a `form` patch value for update_properties() and strips the 367 * `fields` key, which is managed by update_form_fields(). 368 * 369 * @since 7.1.0 370 * 371 * @param mixed $value The incoming `form` patch value. 372 * @return array|null The form properties to merge, or null when the value 373 * is off-shape. 374 */ 375 private function extract_form_properties( $value ) { 376 if ( ! is_array( $value ) || ( array() !== $value && array_is_list( $value ) ) ) { 377 _doing_it_wrong( 378 'WP_View_Config_Data::update_properties', 379 esc_html__( 'A "form" patch must be an associative array of form properties.' ), 380 '7.1.0' 381 ); 382 return null; 383 } 384 if ( array_key_exists( 'fields', $value ) ) { 385 _doing_it_wrong( 386 'WP_View_Config_Data::update_properties', 387 esc_html__( 'The form "fields" are patched by identity. Use update_form_fields() instead.' ), 388 '7.1.0' 389 ); 390 unset( $value['fields'] ); 391 } 392 393 return $value; 394 } 395 396 /** 397 * Recursively merges two values. 398 * 399 * Associative arrays (maps) merge key by key and a null patch value deletes 400 * the key; lists and scalars are replaced wholesale by the incoming value, 401 * since lists without a defined identity cannot be merged member by member. 402 * 403 * @since 7.1.0 404 * 405 * @param mixed $current The current value. 406 * @param mixed $incoming The incoming value. 407 * @return mixed The merged value. 408 */ 409 private function deep_merge( $current, $incoming ) { 410 // An empty array counts as a list, so patching with array() empties 411 // the key (e.g. 'filters' => array() clears the filters) rather than 412 // being a no-op map merge. 413 if ( ! is_array( $incoming ) || array_is_list( $incoming ) ) { 414 return $incoming; 415 } 416 417 // Merge onto the current map, or onto an empty base when the current 418 // value is absent, empty, or not a map, so null delete-markers in the 419 // patch are consumed rather than stored as literal values (e.g. 420 // array( 'layout' => null ) merged into an empty layouts entry yields 421 // array(), not array( 'layout' => null )). 422 $result = is_array( $current ) && ! array_is_list( $current ) ? $current : array(); 423 foreach ( $incoming as $key => $value ) { 424 if ( null === $value ) { 425 // A null patch value deletes the key. 426 unset( $result[ $key ] ); 427 continue; 428 } 429 $result[ $key ] = $this->deep_merge( 430 array_key_exists( $key, $result ) ? $result[ $key ] : array(), 431 $value 432 ); 433 } 434 return $result; 435 } 436 437 /** 438 * Merges a map of field patches into a field list by identity. 439 * 440 * Shared by the top-level `form` fields and a group's `children`: a `null` 441 * value removes the matching field (recursing into children), a map value 442 * merges into the matching field wherever it lives, and an unknown id 443 * appends a new field to the end of this list. A `null` for an id that is 444 * not found is a silent no-op. 445 * 446 * @since 7.1.0 447 * 448 * @param array $current The current list of fields. 449 * @param array $patches The field patches, keyed by field id. 450 * @return array The merged list of fields. 451 */ 452 private function merge_fields_by_identity( array $current, array $patches ) { 453 foreach ( $patches as $id => $value ) { 454 // PHP casts numeric-string array keys to integers; identities are strings. 455 $id = (string) $id; 456 457 if ( null === $value ) { 458 $current = $this->reject_fields( $current, array( $id ) ); 459 continue; 460 } 461 if ( ! is_array( $value ) || ( array() !== $value && array_is_list( $value ) ) ) { 462 _doing_it_wrong( 463 'WP_View_Config_Data::update_form_fields', 464 esc_html__( 'Each field patch must be an associative array of field properties, or null to remove the field.' ), 465 '7.1.0' 466 ); 467 continue; 468 } 469 470 // The patch key is the identity. 471 unset( $value['id'] ); 472 473 $merged = $this->merge_field_in_tree( $current, $id, $value ); 474 if ( null !== $merged ) { 475 $current = $merged; 476 continue; 477 } 478 // An unknown id appends: as a bare string reference when the patch 479 // carries no overrides, as an array otherwise. 480 $current[] = array() === $value ? $id : $this->merge_field_item( $id, $id, $value ); 481 } 482 483 return $current; 484 } 485 486 /** 487 * Merges a field patch into the field carrying the given identity, wherever 488 * it lives in the tree. 489 * 490 * Fields are visited in document order and a group is checked before its 491 * own children, so when an id appears at both levels the group wins. 492 * 493 * @since 7.1.0 494 * 495 * @param array $fields The list of fields to search. 496 * @param string $id The identity of the field to patch. 497 * @param array $value The field patch. 498 * @return array|null The updated list, or null when the id was not found. 499 */ 500 private function merge_field_in_tree( array $fields, $id, array $value ) { 501 foreach ( $fields as $index => $field ) { 502 if ( $this->field_identity( $field ) === $id ) { 503 $fields[ $index ] = $this->merge_field_item( $field, $id, $value ); 504 return $fields; 505 } 506 if ( is_array( $field ) && isset( $field['children'] ) && is_array( $field['children'] ) ) { 507 $children = $this->merge_field_in_tree( $field['children'], $id, $value ); 508 if ( null !== $children ) { 509 $fields[ $index ]['children'] = $children; 510 return $fields; 511 } 512 } 513 } 514 515 return null; 516 } 517 518 /** 519 * Merges a field patch into an existing field. 520 * 521 * A bare string reference is promoted to an array so the overrides apply. 522 * The `children` key follows the same rules — a map merges into the 523 * group's children by id, a list replaces them wholesale, and `null` 524 * deletes the key — and every other key merges via deep_merge(). 525 * 526 * @since 7.1.0 527 * 528 * @param array|string $existing The existing field. 529 * @param string $id The field identity. 530 * @param array $value The field patch. 531 * @return array|string The merged field. 532 */ 533 private function merge_field_item( $existing, $id, array $value ) { 534 if ( ! is_array( $existing ) ) { 535 // Nothing to apply: keep the bare string reference. 536 if ( array() === $value ) { 537 return $existing; 538 } 539 // Promote the reference so the incoming overrides apply. 540 $existing = array( 'id' => $id ); 541 } 542 543 foreach ( $value as $key => $item ) { 544 if ( 'children' === $key ) { 545 if ( null === $item ) { 546 unset( $existing['children'] ); 547 continue; 548 } 549 if ( ! is_array( $item ) ) { 550 _doing_it_wrong( 551 'WP_View_Config_Data::update_form_fields', 552 esc_html__( 'A "children" patch must be an associative array keyed by field id to merge, a numerically indexed array to replace the children wholesale, or null to delete the key.' ), 553 '7.1.0' 554 ); 555 continue; 556 } 557 // A list replaces the children wholesale (an empty array counts 558 // as a list, clearing them)... 559 if ( array_is_list( $item ) ) { 560 $existing['children'] = $item; 561 continue; 562 } 563 // ...and a map merges into them by identity. 564 $children = isset( $existing['children'] ) && is_array( $existing['children'] ) ? $existing['children'] : array(); 565 $existing['children'] = $this->merge_fields_by_identity( $children, $item ); 566 continue; 567 } 568 if ( null === $item ) { 569 // A null patch value deletes the key. 570 unset( $existing[ $key ] ); 571 continue; 572 } 573 $existing[ $key ] = $this->deep_merge( 574 array_key_exists( $key, $existing ) ? $existing[ $key ] : array(), 575 $item 576 ); 577 } 578 579 return $existing; 580 } 581 582 /** 583 * Returns a field list with the fields matching the given identities removed, 584 * recursing into group children. 585 * 586 * @since 7.1.0 587 * 588 * @param array $fields The list of fields. 589 * @param string[] $ids The identities of the fields to remove. 590 * @return array The list with the matching fields removed. 591 */ 592 private function reject_fields( array $fields, array $ids ) { 593 $result = array(); 594 foreach ( $fields as $field ) { 595 if ( in_array( $this->field_identity( $field ), $ids, true ) ) { 596 continue; 597 } 598 if ( is_array( $field ) && isset( $field['children'] ) && is_array( $field['children'] ) ) { 599 $field['children'] = $this->reject_fields( $field['children'], $ids ); 600 } 601 $result[] = $field; 602 } 603 return $result; 604 } 605 606 /** 607 * Resolves the identity of a form field. 608 * 609 * A bare string is its own identity; an object is identified by its `id`. 610 * 611 * @since 7.1.0 612 * 613 * @param mixed $field The field. 614 * @return string|null The identity, or null if it cannot be resolved. 615 */ 616 private function field_identity( $field ) { 617 if ( is_string( $field ) ) { 618 return $field; 619 } 620 if ( is_array( $field ) && isset( $field['id'] ) && is_string( $field['id'] ) ) { 621 return $field['id']; 622 } 623 return null; 624 } 625 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Thu Jul 9 08:20:14 2026 | Cross-referenced by PHPXref |