| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Entity Fields API: WP_Entity_Fields_Registry class 4 * 5 * @package WordPress 6 * @subpackage Entity Fields API 7 * @since 7.2.0 8 */ 9 10 /** 11 * Holds the fields registered for entities. 12 * 13 * The registry maps an entity, identified by its kind and name, to its 14 * registered field definitions keyed by id, in registration order, each 15 * with the origins that registered and updated it, and to the script modules 16 * registered for it, each with the ids of the fields it applies to. 17 * register() adds fields, update() changes registered ones, and both 18 * validate the definitions before storing them: a field that cannot be 19 * stored is reported and skipped, and the rest of the call is stored. 20 * register_collection() adds the fields a folder declares, see it for the 21 * format. 22 * 23 * Fields are registered on the `wp_entity_fields_api_init` action, and only 24 * there, with wp_register_entity_fields(), wp_update_entity_fields(), and 25 * wp_unregister_entity_fields(): register(), update(), and unregister() refuse 26 * to run while the action is not firing. 27 * 28 * The registry is filled lazily: the first time its fields are read, it 29 * fires the `wp_entity_fields_api_init` action, on which the default fields of every 30 * post type and the fields of plugins are registered. A read before `init` 31 * has completed is refused, see initialize(). 32 * 33 * Once the action has fired the registry does not change: every reader of 34 * a request, the REST controller as well as the import map of the editor 35 * script, sees the same fields. 36 * 37 * @since 7.2.0 38 * 39 * @access private 40 */ 41 class WP_Entity_Fields_Registry { 42 43 /** 44 * Registered fields, as kind => name => array of field id => definition. 45 * 46 * Kept nested rather than under a joined `{$kind}/{$name}` key: both parts 47 * may contain any character, so no separator keeps two entities apart. 48 * 49 * @since 7.2.0 50 * @var array<string, array<string, array<string, array>>> 51 */ 52 protected $fields = array(); 53 54 /** 55 * Registered script modules, as kind => name => array of module id => 56 * list of the ids of the fields the module applies to, in registration 57 * order. 58 * 59 * @since 7.2.0 60 * @var array<string, array<string, array<string, string[]>>> 61 */ 62 protected $field_modules = array(); 63 64 /** 65 * Whether the `wp_entity_fields_api_init` action has fired since the registry 66 * was created. 67 * 68 * @since 7.2.0 69 * @var bool 70 */ 71 protected $initialized = false; 72 73 /** 74 * The singleton instance. 75 * 76 * @since 7.2.0 77 * @var WP_Entity_Fields_Registry|null 78 */ 79 protected static $instance = null; 80 81 /** 82 * Returns the singleton instance. 83 * 84 * @since 7.2.0 85 * 86 * @return WP_Entity_Fields_Registry The registry. 87 */ 88 public static function get_instance() { 89 if ( null === self::$instance ) { 90 self::$instance = new self(); 91 } 92 93 return self::$instance; 94 } 95 96 /** 97 * Registers fields for an entity. 98 * 99 * The fields must be new: a field with the id of a registered field is 100 * reported and skipped, and so is every definition after the first of an 101 * id that appears more than once in the call. The rest of the fields of 102 * the call are registered: a field another plugin got to first does not 103 * cost a plugin the others. A definition that is not an array with a 104 * non-empty string `id` is reported and skipped the same way. To change 105 * a registered field, see update(); to replace it, unregister it first. 106 * The script module, if any, applies to every field the call registers. 107 * A field whose `type` DataViews does not provide is reported and 108 * registered anyway. 109 * 110 * The origin is stored as the `origin` property of each field, as 111 * `registeredBy`, replacing any `origin` the definition sets. 112 * 113 * @since 7.2.0 114 * 115 * @param string $origin Who registers the fields: `core`, or 116 * the slug of the plugin or theme. 117 * @param string $kind The entity kind (e.g. `postType`). 118 * @param string $name The entity name (e.g. `page`). 119 * @param array[] $fields The list of field definitions, each with an `id`. 120 * @param string|null $script_module The id of the script module providing 121 * the JavaScript parts of the fields, if any. 122 * @return string[] The ids of the fields registered, in the order of the 123 * call. Empty when none is, when called outside the 124 * `wp_entity_fields_api_init` action, or when an argument is 125 * invalid. 126 */ 127 public function register( $origin, $kind, $name, $fields, $script_module = null ) { 128 if ( ! $this->validate_arguments( __METHOD__, $origin, $kind, $name, $fields, $script_module ) ) { 129 return array(); 130 } 131 $fields = $this->skip_invalid_definitions( __METHOD__, $kind, $name, $fields ); 132 if ( ! $fields ) { 133 return array(); 134 } 135 136 $new_fields = array(); 137 $duplicated = array(); 138 $registered_ids = array(); 139 foreach ( $fields as $field ) { 140 $id = $field['id']; 141 if ( isset( $this->fields[ $kind ][ $name ][ $id ] ) ) { 142 $registered_ids[ $id ] = $id; 143 } elseif ( isset( $new_fields[ $id ] ) ) { 144 $duplicated[ $id ] = $id; 145 } else { 146 $new_fields[ $id ] = $field; 147 } 148 } 149 150 if ( $duplicated ) { 151 _doing_it_wrong( 152 __METHOD__, 153 sprintf( 154 /* translators: 1: Entity kind, e.g. postType. 2: Entity name, e.g. page. 3: Comma-separated list of field ids. */ 155 __( 'A field can only be registered once. These fields of %1$s "%2$s" appear more than once in the same call: %3$s. Only their first definition is registered.' ), 156 $kind, 157 $name, 158 implode( ', ', $duplicated ) 159 ), 160 '7.2.0' 161 ); 162 } 163 164 if ( $registered_ids ) { 165 _doing_it_wrong( 166 __METHOD__, 167 sprintf( 168 /* translators: 1: Entity kind, e.g. postType. 2: Entity name, e.g. page. 3: Comma-separated list of field ids. 4: update() */ 169 __( 'A field can only be registered once. These fields of %1$s "%2$s" are skipped because they are already registered: %3$s. Use %4$s to change a registered field, or unregister it first to replace it.' ), 170 $kind, 171 $name, 172 implode( ', ', $registered_ids ), 173 '<code>update()</code>' 174 ), 175 '7.2.0' 176 ); 177 } 178 179 if ( ! $new_fields ) { 180 return array(); 181 } 182 $this->report_unknown_types( __METHOD__, $kind, $name, $new_fields ); 183 184 foreach ( $new_fields as $field ) { 185 $this->fields[ $kind ][ $name ][ $field['id'] ] = array_merge( 186 $field, 187 array( 188 'origin' => array( 189 'registeredBy' => $origin, 190 'updatedBy' => array(), 191 ), 192 ) 193 ); 194 } 195 $ids = array_column( $new_fields, 'id' ); 196 $this->add_field_module( $kind, $name, $ids, $script_module ); 197 198 return $ids; 199 } 200 201 /** 202 * Updates registered fields of an entity. 203 * 204 * Each definition is merged into the registered field with its id, 205 * property by property; the field keeps its position. The fields must be 206 * registered: a field that is not is reported and skipped, and so is a 207 * definition that is not an array with a non-empty string `id`; the rest 208 * of the fields of the call are updated. The script module, if any, 209 * applies to every field the call updates, on top of the modules the 210 * fields have. A field whose `type` DataViews does not provide is 211 * reported and updated anyway. 212 * 213 * The origin is appended to the `updatedBy` list of the `origin` property 214 * of each field, once; the rest of the `origin` property cannot be 215 * updated, and any `origin` the definition sets is ignored. 216 * 217 * Like register(), it only runs on the `wp_entity_fields_api_init` action. 218 * 219 * @since 7.2.0 220 * 221 * @param string $origin Who updates the fields: `core`, or 222 * the slug of the plugin or theme. 223 * @param string $kind The entity kind (e.g. `postType`). 224 * @param string $name The entity name (e.g. `page`). 225 * @param array[] $fields The list of partial field definitions, 226 * each with the `id` of a registered field. 227 * @param string|null $script_module The id of the script module providing 228 * the JavaScript parts of the fields, if any. 229 * @return string[] The ids of the fields updated, in the order of the 230 * call. Empty when none is, when called outside the 231 * `wp_entity_fields_api_init` action, or when an argument is 232 * invalid. 233 */ 234 public function update( $origin, $kind, $name, $fields, $script_module = null ) { 235 if ( ! $this->validate_arguments( __METHOD__, $origin, $kind, $name, $fields, $script_module ) ) { 236 return array(); 237 } 238 $fields = $this->skip_invalid_definitions( __METHOD__, $kind, $name, $fields ); 239 if ( ! $fields ) { 240 return array(); 241 } 242 $this->report_unknown_types( __METHOD__, $kind, $name, $fields ); 243 244 $ids = array(); 245 $unregistered_ids = array(); 246 foreach ( $fields as $field ) { 247 $id = $field['id']; 248 if ( ! isset( $this->fields[ $kind ][ $name ][ $id ] ) ) { 249 $unregistered_ids[ $id ] = $id; 250 continue; 251 } 252 253 $registered = $this->fields[ $kind ][ $name ][ $id ]; 254 $field_origin = $registered['origin']; 255 if ( ! in_array( $origin, $field_origin['updatedBy'], true ) ) { 256 $field_origin['updatedBy'][] = $origin; 257 } 258 259 $this->fields[ $kind ][ $name ][ $id ] = array_merge( $registered, $field, array( 'origin' => $field_origin ) ); 260 if ( ! in_array( $id, $ids, true ) ) { 261 $ids[] = $id; 262 } 263 } 264 265 if ( $unregistered_ids ) { 266 _doing_it_wrong( 267 __METHOD__, 268 sprintf( 269 /* translators: 1: Entity kind, e.g. postType. 2: Entity name, e.g. page. 3: Comma-separated list of field ids. 4: register() */ 270 __( 'Only registered fields can be updated. These fields of %1$s "%2$s" are skipped because they are not registered: %3$s. Use %4$s to add a field.' ), 271 $kind, 272 $name, 273 implode( ', ', $unregistered_ids ), 274 '<code>register()</code>' 275 ), 276 '7.2.0' 277 ); 278 } 279 280 if ( $ids ) { 281 $this->add_field_module( $kind, $name, $ids, $script_module ); 282 } 283 284 return $ids; 285 } 286 287 /** 288 * Unregisters fields of an entity. 289 * 290 * Unregistering a field removes its registered definition and 291 * drops it from the script modules that applied to it (a module left with 292 * no field is forgotten). Unregistering every field forgets the entity: 293 * its registered fields and script modules. 294 * 295 * Like register(), it only runs on the `wp_entity_fields_api_init` action, and an 296 * invalid entity is reported and unregisters nothing. An id that is not a 297 * non-empty string is reported and skipped, like a definition register() 298 * cannot store. 299 * 300 * @since 7.2.0 301 * 302 * @param string $kind The entity kind (e.g. `postType`). 303 * @param string $name The entity name (e.g. `page`). 304 * @param string[]|null $ids The ids of the fields to unregister. Default 305 * null, every field of the entity. 306 * @return array[] The list of the definitions unregistered, in 307 * registration order, as get_registered() returns them. 308 * Empty when none of the fields is registered, when 309 * called outside the `wp_entity_fields_api_init` action, or when 310 * the entity is invalid. 311 */ 312 public function unregister( $kind, $name, $ids = null ) { 313 if ( ! $this->doing_entity_fields_api_init( __METHOD__ ) || ! $this->validate_entity( __METHOD__, $kind, $name ) ) { 314 return array(); 315 } 316 317 if ( null === $ids ) { 318 $unregistered = array_values( $this->fields[ $kind ][ $name ] ?? array() ); 319 unset( $this->fields[ $kind ][ $name ], $this->field_modules[ $kind ][ $name ] ); 320 $this->forget_empty_entity( $kind, $name ); 321 return $unregistered; 322 } 323 324 $ids = $this->skip_invalid_ids( __METHOD__, $kind, $name, (array) $ids ); 325 if ( ! $ids ) { 326 return array(); 327 } 328 329 $unregistered = array_intersect_key( $this->fields[ $kind ][ $name ] ?? array(), array_flip( $ids ) ); 330 if ( empty( $unregistered ) ) { 331 return array(); 332 } 333 334 // A script module only ever applies to registered fields: register() 335 // and update() add it for fields they store, and fields leave both 336 // lists together here. So the fields removed are all the registry 337 // loses, and the modules are not returned: the ones that applied to 338 // them are listed by get_registered_field_modules(), read beforehand. 339 $ids = array_keys( $unregistered ); 340 foreach ( $ids as $id ) { 341 unset( $this->fields[ $kind ][ $name ][ $id ] ); 342 } 343 foreach ( $this->field_modules[ $kind ][ $name ] ?? array() as $module => $module_ids ) { 344 $remaining = array_values( array_diff( $module_ids, $ids ) ); 345 if ( empty( $remaining ) ) { 346 unset( $this->field_modules[ $kind ][ $name ][ $module ] ); 347 } else { 348 $this->field_modules[ $kind ][ $name ][ $module ] = $remaining; 349 } 350 } 351 $this->forget_empty_entity( $kind, $name ); 352 353 return array_values( $unregistered ); 354 } 355 356 /** 357 * Registers the fields of a field collection. 358 * 359 * A collection is a folder whose `index.php` returns its configuration, with 360 * a folder per field whose `field.php` returns the serializable part of the 361 * field (see get_collection_fields()). Each file is required without 362 * sharing the variables of the caller, see require_file(). The JavaScript 363 * parts of the fields, if any, ship in the script module of the 364 * collection. The configuration is an array with these keys: 365 * 366 * - `origin` (required): who registers the fields: `core`, or the slug of 367 * the plugin or theme. See register(). 368 * - `kind` (required): the entity kind, e.g. `postType`. 369 * - `name` (required): the entity name, e.g. `wp_template`. 370 * - `module` (optional): the id of the script module providing the 371 * JavaScript parts of the fields. Every field of the collection is 372 * registered with it. 373 * 374 * The collection registers its fields on the entity, after the fields 375 * registered on it before, in the alphabetical order of their folders. 376 * register() skips a field already registered for the entity, and 377 * registers the rest of the collection. Like register(), it does not check 378 * that the entity exists: the `/wp/v2/entity-fields` route only serves the fields 379 * of the entities the REST API exposes. 380 * 381 * Fields that apply to several entities, such as the defaults every post 382 * type derives from its supports, are registered in code: a 383 * `wp_entity_fields_api_init` callback can read their definitions from files with 384 * get_collection_fields() and register them on each entity with 385 * register(). 386 * 387 * Like register(), it only runs on the `wp_entity_fields_api_init` action. An 388 * invalid configuration is reported with _doing_it_wrong() and skipped. So 389 * is an invalid `field.php`, see get_collection_fields(): the rest of the 390 * fields of the collection are registered. 391 * 392 * @since 7.2.0 393 * 394 * @param string $directory The directory of the collection. 395 * @return bool Whether the collection is valid and all of its fields are 396 * registered. True as well when it has no fields; false when 397 * a `field.php` is skipped, or when called outside the 398 * `wp_entity_fields_api_init` action. 399 */ 400 public function register_collection( $directory ) { 401 if ( ! $this->doing_entity_fields_api_init( __METHOD__ ) ) { 402 return false; 403 } 404 405 $collection = $this->get_collection( __METHOD__, $directory ); 406 if ( null === $collection ) { 407 return false; 408 } 409 410 if ( empty( $collection['fields'] ) ) { 411 return ! $collection['skipped']; 412 } 413 $registered = $this->register( $collection['origin'], $collection['kind'], $collection['name'], $collection['fields'], $collection['module'] ); 414 415 return ! $collection['skipped'] && count( $registered ) === count( $collection['fields'] ); 416 } 417 418 /** 419 * Returns the fields of a field collection. 420 * 421 * A collection holds a folder per field, whose `field.php` returns the 422 * serializable part of the field, see register_collection(). 423 * 424 * @since 7.2.0 425 * 426 * @param string $directory The directory of the collection. 427 * @return array<string, array> The fields of the collection, keyed by id, in 428 * the alphabetical order of their folders. The 429 * id of a field is the name of its folder, 430 * unless its `field.php` sets an `id`, and it 431 * comes first in the field. A `field.php` 432 * that does not return an array with a 433 * non-empty string id, or whose id an earlier 434 * folder took, is reported and skipped. 435 */ 436 public function get_collection_fields( $directory ) { 437 return $this->read_collection_fields( __METHOD__, $directory )['fields']; 438 } 439 440 /** 441 * Returns the fields registered for an entity. 442 * 443 * @since 7.2.0 444 * 445 * @param string $kind The entity kind (e.g. `postType`). 446 * @param string $name The entity name (e.g. `page`). 447 * @return array[] The list of field definitions, in registration order. 448 */ 449 public function get_registered( $kind, $name ) { 450 $this->initialize(); 451 return array_values( $this->fields[ $kind ][ $name ] ?? array() ); 452 } 453 454 /** 455 * Returns the fields registered for every entity. 456 * 457 * @since 7.2.0 458 * 459 * @return array<string, array<string, array[]>> The lists of field 460 * definitions, keyed by 461 * kind, then by name. 462 */ 463 public function get_all_registered() { 464 $this->initialize(); 465 return array_map( 466 static function ( $entities ) { 467 return array_map( 'array_values', $entities ); 468 }, 469 $this->fields 470 ); 471 } 472 473 /** 474 * Returns the script modules registered for an entity. 475 * 476 * @since 7.2.0 477 * 478 * @param string $kind The entity kind (e.g. `postType`). 479 * @param string $name The entity name (e.g. `page`). 480 * @return array<string, string[]> The ids of the fields each module 481 * applies to, keyed by module id, in 482 * registration order. 483 */ 484 public function get_registered_field_modules( $kind, $name ) { 485 $this->initialize(); 486 return $this->field_modules[ $kind ][ $name ] ?? array(); 487 } 488 489 /** 490 * Returns the ids of the script modules registered for every entity. 491 * 492 * @since 7.2.0 493 * 494 * @return array<string, array<string, string[]>> The lists of module ids, 495 * keyed by kind, then by 496 * name. 497 */ 498 public function get_all_registered_field_modules() { 499 $this->initialize(); 500 return array_map( 501 static function ( $entities ) { 502 return array_map( 'array_keys', $entities ); 503 }, 504 $this->field_modules 505 ); 506 } 507 508 /** 509 * Fires the `wp_entity_fields_api_init` action the first time the registry 510 * is read. 511 * 512 * The read is refused until `init` has completed. The registry holds the 513 * fields of post types, and post types are not known until then: core 514 * registers its own on `init` at priority 0, plugins theirs at the default 515 * priority, and supports are added and removed on `init` too. The default 516 * fields of a post type derive from its supports, and the action fires 517 * once, so a read before or during `init` would fix the defaults from the 518 * post types registered so far for the rest of the request. 519 * 520 * did_action() is true from the first `init` callback on, hence the 521 * doing_action() check for the reads made while it runs. 522 * 523 * @since 7.2.0 524 */ 525 protected function initialize() { 526 if ( $this->initialized ) { 527 return; 528 } 529 530 if ( ! did_action( 'init' ) || doing_action( 'init' ) ) { 531 _doing_it_wrong( 532 __METHOD__, 533 sprintf( 534 /* translators: 1: init, 2: wp_entity_fields_api_init */ 535 __( 'The registered fields cannot be read until the %1$s action has completed: the post types and supports they derive from are registered on %1$s. Read them once %1$s has completed, or hook %2$s.' ), 536 '<code>init</code>', 537 '<code>wp_entity_fields_api_init</code>' 538 ), 539 '7.2.0' 540 ); 541 return; 542 } 543 544 // Set before firing, so a callback reading the registry to inspect 545 // the fields it updates does not fire the action again. 546 $this->initialized = true; 547 548 /** 549 * Fires the first time the registered fields are read, after `init`. 550 * 551 * Register or adjust fields here, with wp_register_entity_fields(), 552 * wp_update_entity_fields(), and wp_unregister_entity_fields(): it is the only 553 * time they run. 554 * 555 * The first read happens wherever the fields are needed: while 556 * handling a REST request as well as on `admin_footer` of the pages 557 * that load the editor script. A callback should register fields and 558 * nothing else; script modules, styles, and their enqueue hooks 559 * belong on `init`. 560 * 561 * @since 7.2.0 562 */ 563 do_action( 'wp_entity_fields_api_init' ); 564 } 565 566 /** 567 * Checks that the `wp_entity_fields_api_init` action is firing, as register(), 568 * update(), and unregister() require. 569 * 570 * Outside the action a registration would either come before the 571 * defaults, which could then not be registered, or after the fields have been read 572 * and the import map of the editor script built from them. Refusing 573 * keeps the registry immutable once read. 574 * 575 * @since 7.2.0 576 * 577 * @param string $method The calling method, for the notice. 578 * @return bool Whether the action is firing. 579 */ 580 protected function doing_entity_fields_api_init( $method ) { 581 if ( doing_action( 'wp_entity_fields_api_init' ) ) { 582 return true; 583 } 584 585 _doing_it_wrong( 586 $method, 587 sprintf( 588 /* translators: %s: wp_entity_fields_api_init */ 589 __( 'Fields can only be registered, updated, and unregistered on the %s action.' ), 590 '<code>wp_entity_fields_api_init</code>' 591 ), 592 '7.2.0' 593 ); 594 return false; 595 } 596 597 /** 598 * Reports the fields whose `type` is not one the REST schema of the 599 * fields lists, with _doing_it_wrong(). Those are the types DataViews 600 * provides a control for: a misspelt type is otherwise silent, the field 601 * renders without a control. A missing `type` is fine. 602 * 603 * @since 7.2.0 604 * 605 * @param string $method The calling method, for the notice. 606 * @param string $kind The entity kind. 607 * @param string $name The entity name. 608 * @param array[] $fields The field definitions. 609 */ 610 protected function report_unknown_types( $method, $kind, $name, $fields ) { 611 $type_schema = ( new WP_REST_Entity_Fields_Controller() )->get_item_schema()['properties']['fields']['items']['properties']['type']; 612 $unknown = array(); 613 foreach ( $fields as $field ) { 614 if ( ! isset( $field['type'] ) ) { 615 continue; 616 } 617 $valid = rest_validate_value_from_schema( $field['type'], $type_schema, 'type' ); 618 if ( is_wp_error( $valid ) ) { 619 $unknown[] = sprintf( '%s (%s)', $field['id'], $valid->get_error_message() ); 620 } 621 } 622 if ( ! $unknown ) { 623 return; 624 } 625 _doing_it_wrong( 626 $method, 627 sprintf( 628 /* translators: 1: Entity kind, e.g. postType. 2: Entity name, e.g. page. 3: Comma-separated list of field ids, each with the error of its type. */ 629 __( 'These fields of %1$s "%2$s" have a type DataViews does not provide, so they render without a control: %3$s' ), 630 $kind, 631 $name, 632 implode( ', ', $unknown ) 633 ), 634 '7.2.0' 635 ); 636 } 637 638 /** 639 * Validates the arguments of register() and update(). 640 * 641 * @since 7.2.0 642 * 643 * @param string $method The calling method, for the notice. 644 * @param mixed $origin The origin. 645 * @param mixed $kind The entity kind. 646 * @param mixed $name The entity name. 647 * @param mixed $fields The field definitions. 648 * @param mixed $script_module The script module id, if any. 649 * @return bool Whether the call may proceed: the `wp_entity_fields_api_init` action 650 * is firing and the arguments are valid. 651 */ 652 protected function validate_arguments( $method, $origin, $kind, $name, $fields, $script_module ) { 653 if ( ! $this->doing_entity_fields_api_init( $method ) ) { 654 return false; 655 } 656 657 if ( ! is_string( $origin ) || '' === $origin ) { 658 _doing_it_wrong( 659 $method, 660 __( 'The origin must be a non-empty string.' ), 661 '7.2.0' 662 ); 663 return false; 664 } 665 666 if ( ! $this->validate_entity( $method, $kind, $name ) ) { 667 return false; 668 } 669 670 if ( ! is_array( $fields ) || empty( $fields ) || ! array_is_list( $fields ) ) { 671 _doing_it_wrong( 672 $method, 673 __( 'The fields must be a list of non-empty field definitions.' ), 674 '7.2.0' 675 ); 676 return false; 677 } 678 679 if ( null !== $script_module && ( ! is_string( $script_module ) || '' === $script_module ) ) { 680 _doing_it_wrong( 681 $method, 682 __( 'The script module must be the id of a script module.' ), 683 '7.2.0' 684 ); 685 return false; 686 } 687 688 return true; 689 } 690 691 /** 692 * Checks that the entity kind and name are non-empty strings, reporting 693 * them with _doing_it_wrong() otherwise. 694 * 695 * @since 7.2.0 696 * 697 * @param string $method The calling method, for the notice. 698 * @param mixed $kind The entity kind. 699 * @param mixed $name The entity name. 700 * @return bool Whether both are valid. 701 */ 702 protected function validate_entity( $method, $kind, $name ) { 703 foreach ( array( $kind, $name ) as $argument ) { 704 if ( ! is_string( $argument ) || '' === $argument ) { 705 _doing_it_wrong( 706 $method, 707 __( 'The entity kind and the entity name must be non-empty strings.' ), 708 '7.2.0' 709 ); 710 return false; 711 } 712 } 713 return true; 714 } 715 716 /** 717 * Drops the definitions that are not an array with a non-empty string 718 * `id`, reporting their positions in the call with _doing_it_wrong(): 719 * like a duplicated field, an invalid definition does not cost the call 720 * its other fields. 721 * 722 * @since 7.2.0 723 * 724 * @param string $method The calling method, for the notice. 725 * @param string $kind The entity kind. 726 * @param string $name The entity name. 727 * @param array[] $fields The list of field definitions. 728 * @return array[] The valid definitions, in the order of the call. 729 */ 730 protected function skip_invalid_definitions( $method, $kind, $name, $fields ) { 731 $valid = array(); 732 $invalid = array(); 733 foreach ( $fields as $position => $field ) { 734 if ( is_array( $field ) && ! empty( $field['id'] ) && is_string( $field['id'] ) ) { 735 $valid[] = $field; 736 } else { 737 $invalid[] = '#' . ( $position + 1 ); 738 } 739 } 740 741 if ( $invalid ) { 742 _doing_it_wrong( 743 $method, 744 sprintf( 745 /* translators: 1: Entity kind, e.g. postType. 2: Entity name, e.g. page. 3: Comma-separated list of positions, e.g. #2, #4. 4: id */ 746 __( 'Every field definition must be an array with a non-empty string %4$s. These definitions of %1$s "%2$s" are skipped: %3$s. The rest of the fields of the call are stored.' ), 747 $kind, 748 $name, 749 implode( ', ', $invalid ), 750 '<code>id</code>' 751 ), 752 '7.2.0' 753 ); 754 } 755 756 return $valid; 757 } 758 759 /** 760 * Drops the ids that are not a non-empty string, reporting their 761 * positions in the call with _doing_it_wrong(): like an invalid 762 * definition, an invalid id does not cost the call its other ids. 763 * 764 * @since 7.2.0 765 * 766 * @param string $method The calling method, for the notice. 767 * @param string $kind The entity kind. 768 * @param string $name The entity name. 769 * @param array $ids The ids of the fields. 770 * @return string[] The valid ids, in the order of the call. 771 */ 772 protected function skip_invalid_ids( $method, $kind, $name, $ids ) { 773 $valid = array(); 774 $invalid = array(); 775 foreach ( array_values( $ids ) as $position => $id ) { 776 if ( is_string( $id ) && '' !== $id ) { 777 $valid[] = $id; 778 } else { 779 $invalid[] = '#' . ( $position + 1 ); 780 } 781 } 782 783 if ( $invalid ) { 784 _doing_it_wrong( 785 $method, 786 sprintf( 787 /* translators: 1: Entity kind, e.g. postType. 2: Entity name, e.g. page. 3: Comma-separated list of positions, e.g. #2, #4. */ 788 __( 'Every field id must be a non-empty string. These ids of %1$s "%2$s" are skipped: %3$s. The rest of the fields of the call are unregistered.' ), 789 $kind, 790 $name, 791 implode( ', ', $invalid ) 792 ), 793 '7.2.0' 794 ); 795 } 796 797 return $valid; 798 } 799 800 /** 801 * Reads and validates a field collection, see register_collection() for 802 * its format. 803 * 804 * An invalid configuration is reported with _doing_it_wrong(): the 805 * collection is skipped. 806 * 807 * @since 7.2.0 808 * 809 * @param string $method The calling method, for the notice. 810 * @param string $directory The directory of the collection. 811 * @return array|null The configuration of the collection, with the `module` 812 * key set, its valid `fields` as a list, and `skipped` 813 * telling whether a `field.php` was skipped, see 814 * read_collection_fields(); or null when the 815 * configuration is invalid. 816 */ 817 protected function get_collection( $method, $directory ) { 818 $slug = basename( $directory ); 819 $file = $directory . '/index.php'; 820 $config = is_file( $file ) ? self::require_file( $file ) : null; 821 $error = null; 822 823 if ( ! is_array( $config ) ) { 824 $error = sprintf( 825 /* translators: %s: index.php */ 826 __( 'The %s of a field collection must return an array.' ), 827 '<code>index.php</code>' 828 ); 829 } elseif ( ! isset( $config['origin'] ) || ! is_string( $config['origin'] ) || '' === $config['origin'] ) { 830 $error = sprintf( 831 /* translators: %s: origin */ 832 __( 'The %s of a field collection must be a non-empty string.' ), 833 '<code>origin</code>' 834 ); 835 } elseif ( ! isset( $config['kind'] ) || ! is_string( $config['kind'] ) || '' === $config['kind'] ) { 836 $error = sprintf( 837 /* translators: %s: kind */ 838 __( 'The %s of a field collection must be a non-empty string.' ), 839 '<code>kind</code>' 840 ); 841 } elseif ( ! isset( $config['name'] ) || ! is_string( $config['name'] ) || '' === $config['name'] ) { 842 $error = sprintf( 843 /* translators: %s: name */ 844 __( 'The %s of a field collection must be a non-empty string.' ), 845 '<code>name</code>' 846 ); 847 } elseif ( isset( $config['module'] ) && ( ! is_string( $config['module'] ) || '' === $config['module'] ) ) { 848 $error = sprintf( 849 /* translators: %s: module */ 850 __( 'The %s of a field collection must be the id of a script module.' ), 851 '<code>module</code>' 852 ); 853 } 854 855 if ( null !== $error ) { 856 _doing_it_wrong( 857 $method, 858 /* translators: 1: The name of a field collection. 2: The error. */ 859 sprintf( __( 'The field collection "%1$s" is skipped. %2$s' ), $slug, $error ), 860 '7.2.0' 861 ); 862 return null; 863 } 864 865 $read = $this->read_collection_fields( $method, $directory ); 866 867 return array_merge( 868 $config, 869 array( 870 'module' => $config['module'] ?? null, 871 'fields' => array_values( $read['fields'] ), 872 'skipped' => $read['skipped'], 873 ) 874 ); 875 } 876 877 /** 878 * Reads the fields of a field collection, see get_collection_fields(). 879 * 880 * The id of a field is used as a key, so it is checked before: a 881 * `field.php` that does not return an array with a non-empty string id 882 * is reported with _doing_it_wrong() and skipped, and so is one whose 883 * id an earlier folder of the collection took. The rest of the fields 884 * are read. 885 * 886 * @since 7.2.0 887 * 888 * @param string $method The calling method, for the notice. 889 * @param string $directory The directory of the collection. 890 * @return array { 891 * The fields of the collection. 892 * 893 * @type array<string, array> $fields The valid fields, keyed by id, 894 * in the alphabetical order of 895 * their folders. 896 * @type bool $skipped Whether a `field.php` was skipped. 897 * } 898 */ 899 protected function read_collection_fields( $method, $directory ) { 900 $files = glob( $directory . '/*/field.php' ); 901 if ( empty( $files ) ) { 902 return array( 903 'fields' => array(), 904 'skipped' => false, 905 ); 906 } 907 sort( $files, SORT_STRING ); 908 909 $fields = array(); 910 $invalid = array(); 911 $duplicated = array(); 912 foreach ( $files as $file ) { 913 $folder = basename( dirname( $file ) ); 914 $field = self::require_file( $file ); 915 if ( ! is_array( $field ) ) { 916 $invalid[] = $folder; 917 continue; 918 } 919 920 // The id comes first, like in the fields registered directly. 921 $field = array_merge( array( 'id' => $folder ), $field ); 922 $id = $field['id']; 923 if ( ! is_string( $id ) || '' === $id ) { 924 $invalid[] = $folder; 925 } elseif ( isset( $fields[ $id ] ) ) { 926 $duplicated[] = $folder; 927 } else { 928 $fields[ $id ] = $field; 929 } 930 } 931 932 $collection = basename( $directory ); 933 if ( $invalid ) { 934 _doing_it_wrong( 935 $method, 936 sprintf( 937 /* translators: 1: The name of a field collection. 2: Comma-separated list of folder names. 3: field.php 4: id */ 938 __( 'The %3$s of a field must return an array, with a non-empty string %4$s if it sets one. These fields of the field collection "%1$s" are skipped: %2$s.' ), 939 $collection, 940 implode( ', ', $invalid ), 941 '<code>field.php</code>', 942 '<code>id</code>' 943 ), 944 '7.2.0' 945 ); 946 } 947 948 if ( $duplicated ) { 949 _doing_it_wrong( 950 $method, 951 sprintf( 952 /* translators: 1: The name of a field collection. 2: Comma-separated list of folder names. */ 953 __( 'A field can only be defined once in a field collection. These fields of the field collection "%1$s" are skipped because an earlier folder defines their id: %2$s.' ), 954 $collection, 955 implode( ', ', $duplicated ) 956 ), 957 '7.2.0' 958 ); 959 } 960 961 return array( 962 'fields' => $fields, 963 'skipped' => $invalid || $duplicated, 964 ); 965 } 966 967 /** 968 * Requires a file without sharing the variables of the caller: this is 969 * about variable scope. 970 * 971 * @since 7.2.0 972 * 973 * @param string $file The path of the file. 974 * @return mixed The return value of the file. 975 */ 976 protected static function require_file( $file ) { 977 return require $file; 978 } 979 980 /** 981 * Applies a script module to fields of an entity, after the fields it 982 * already applies to. 983 * 984 * @since 7.2.0 985 * 986 * @param string $kind The entity kind. 987 * @param string $name The entity name. 988 * @param string[] $ids The ids of the fields. 989 * @param string|null $script_module The id of the script module, if any. 990 */ 991 protected function add_field_module( $kind, $name, $ids, $script_module ) { 992 if ( null === $script_module ) { 993 return; 994 } 995 996 $module_ids = $this->field_modules[ $kind ][ $name ][ $script_module ] ?? array(); 997 foreach ( $ids as $id ) { 998 if ( ! in_array( $id, $module_ids, true ) ) { 999 $module_ids[] = $id; 1000 } 1001 } 1002 $this->field_modules[ $kind ][ $name ][ $script_module ] = $module_ids; 1003 } 1004 1005 /** 1006 * Forgets an entity left without fields or script modules, and its kind 1007 * once no entity of the kind is left, so the registry only lists 1008 * entities that have some. 1009 * 1010 * @since 7.2.0 1011 * 1012 * @param string $kind The entity kind. 1013 * @param string $name The entity name. 1014 */ 1015 protected function forget_empty_entity( $kind, $name ) { 1016 if ( empty( $this->fields[ $kind ][ $name ] ) ) { 1017 unset( $this->fields[ $kind ][ $name ] ); 1018 } 1019 if ( empty( $this->fields[ $kind ] ) ) { 1020 unset( $this->fields[ $kind ] ); 1021 } 1022 if ( empty( $this->field_modules[ $kind ][ $name ] ) ) { 1023 unset( $this->field_modules[ $kind ][ $name ] ); 1024 } 1025 if ( empty( $this->field_modules[ $kind ] ) ) { 1026 unset( $this->field_modules[ $kind ] ); 1027 } 1028 } 1029 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Oct 20 08:20:34 2026 | Cross-referenced by PHPXref |