| [ 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[] { 481 * The script modules, in registration order. 482 * 483 * @type array ...$0 { 484 * @type string $id The id of the script module. 485 * @type string[] $fields The ids of the fields the module applies 486 * to, in registration order. 487 * } 488 * } 489 */ 490 public function get_registered_field_modules( $kind, $name ) { 491 $this->initialize(); 492 $modules = array(); 493 foreach ( $this->field_modules[ $kind ][ $name ] ?? array() as $module => $field_ids ) { 494 $modules[] = array( 495 // A numeric module id is an integer as an array key. 496 'id' => (string) $module, 497 'fields' => $field_ids, 498 ); 499 } 500 return $modules; 501 } 502 503 /** 504 * Returns the ids of the script modules registered for every entity. 505 * 506 * @since 7.2.0 507 * 508 * @return array<string, array<string, string[]>> The lists of module ids, 509 * keyed by kind, then by 510 * name. 511 */ 512 public function get_all_registered_field_modules() { 513 $this->initialize(); 514 /* 515 * Keeps the module ids and drops the field ids they apply to, e.g.: 516 * 517 * array( 518 * 'postType' => array( 519 * 'page' => array( 'my-plugin/fields', '123' ), 520 * 'post' => array( 'my-plugin/fields' ), 521 * ), 522 * ) 523 */ 524 return array_map( 525 static function ( $entities ) { 526 return array_map( 527 static function ( $modules ) { 528 // A numeric module id is an integer as an array key. 529 return array_map( 'strval', array_keys( $modules ) ); 530 }, 531 $entities 532 ); 533 }, 534 $this->field_modules 535 ); 536 } 537 538 /** 539 * Fires the `wp_entity_fields_api_init` action the first time the registry 540 * is read. 541 * 542 * The read is refused until `init` has completed. The registry holds the 543 * fields of post types, and post types are not known until then: core 544 * registers its own on `init` at priority 0, plugins theirs at the default 545 * priority, and supports are added and removed on `init` too. The default 546 * fields of a post type derive from its supports, and the action fires 547 * once, so a read before or during `init` would fix the defaults from the 548 * post types registered so far for the rest of the request. 549 * 550 * did_action() is true from the first `init` callback on, hence the 551 * doing_action() check for the reads made while it runs. 552 * 553 * @since 7.2.0 554 */ 555 protected function initialize() { 556 if ( $this->initialized ) { 557 return; 558 } 559 560 if ( ! did_action( 'init' ) || doing_action( 'init' ) ) { 561 _doing_it_wrong( 562 __METHOD__, 563 sprintf( 564 /* translators: 1: init, 2: wp_entity_fields_api_init */ 565 __( '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.' ), 566 '<code>init</code>', 567 '<code>wp_entity_fields_api_init</code>' 568 ), 569 '7.2.0' 570 ); 571 return; 572 } 573 574 // Set before firing, so a callback reading the registry to inspect 575 // the fields it updates does not fire the action again. 576 $this->initialized = true; 577 578 /** 579 * Fires the first time the registered fields are read, after `init`. 580 * 581 * Register or adjust fields here, with wp_register_entity_fields(), 582 * wp_update_entity_fields(), and wp_unregister_entity_fields(): it is the only 583 * time they run. 584 * 585 * The first read happens wherever the fields are needed: while 586 * handling a REST request as well as on `admin_footer` of the pages 587 * that load the editor script. A callback should register fields and 588 * nothing else; script modules, styles, and their enqueue hooks 589 * belong on `init`. 590 * 591 * @since 7.2.0 592 */ 593 do_action( 'wp_entity_fields_api_init' ); 594 } 595 596 /** 597 * Checks that the `wp_entity_fields_api_init` action is firing, as register(), 598 * update(), and unregister() require. 599 * 600 * Outside the action a registration would either come before the 601 * defaults, which could then not be registered, or after the fields have been read 602 * and the import map of the editor script built from them. Refusing 603 * keeps the registry immutable once read. 604 * 605 * @since 7.2.0 606 * 607 * @param string $method The calling method, for the notice. 608 * @return bool Whether the action is firing. 609 */ 610 protected function doing_entity_fields_api_init( $method ) { 611 if ( doing_action( 'wp_entity_fields_api_init' ) ) { 612 return true; 613 } 614 615 _doing_it_wrong( 616 $method, 617 sprintf( 618 /* translators: %s: wp_entity_fields_api_init */ 619 __( 'Fields can only be registered, updated, and unregistered on the %s action.' ), 620 '<code>wp_entity_fields_api_init</code>' 621 ), 622 '7.2.0' 623 ); 624 return false; 625 } 626 627 /** 628 * Reports the fields whose `type` is not one the REST schema of the 629 * fields lists, with _doing_it_wrong(). Those are the types DataViews 630 * provides a control for: a misspelt type is otherwise silent, the field 631 * renders without a control. A missing `type` is fine. 632 * 633 * @since 7.2.0 634 * 635 * @param string $method The calling method, for the notice. 636 * @param string $kind The entity kind. 637 * @param string $name The entity name. 638 * @param array[] $fields The field definitions. 639 */ 640 protected function report_unknown_types( $method, $kind, $name, $fields ) { 641 $type_schema = ( new WP_REST_Entity_Fields_Controller() )->get_item_schema()['properties']['fields']['items']['properties']['type']; 642 $unknown = array(); 643 foreach ( $fields as $field ) { 644 if ( ! isset( $field['type'] ) ) { 645 continue; 646 } 647 $valid = rest_validate_value_from_schema( $field['type'], $type_schema, 'type' ); 648 if ( is_wp_error( $valid ) ) { 649 $unknown[] = sprintf( '%s (%s)', $field['id'], $valid->get_error_message() ); 650 } 651 } 652 if ( ! $unknown ) { 653 return; 654 } 655 _doing_it_wrong( 656 $method, 657 sprintf( 658 /* 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. */ 659 __( 'These fields of %1$s "%2$s" have a type DataViews does not provide, so they render without a control: %3$s' ), 660 $kind, 661 $name, 662 implode( ', ', $unknown ) 663 ), 664 '7.2.0' 665 ); 666 } 667 668 /** 669 * Validates the arguments of register() and update(). 670 * 671 * @since 7.2.0 672 * 673 * @param string $method The calling method, for the notice. 674 * @param mixed $origin The origin. 675 * @param mixed $kind The entity kind. 676 * @param mixed $name The entity name. 677 * @param mixed $fields The field definitions. 678 * @param mixed $script_module The script module id, if any. 679 * @return bool Whether the call may proceed: the `wp_entity_fields_api_init` action 680 * is firing and the arguments are valid. 681 */ 682 protected function validate_arguments( $method, $origin, $kind, $name, $fields, $script_module ) { 683 if ( ! $this->doing_entity_fields_api_init( $method ) ) { 684 return false; 685 } 686 687 if ( ! is_string( $origin ) || '' === $origin ) { 688 _doing_it_wrong( 689 $method, 690 __( 'The origin must be a non-empty string.' ), 691 '7.2.0' 692 ); 693 return false; 694 } 695 696 if ( ! $this->validate_entity( $method, $kind, $name ) ) { 697 return false; 698 } 699 700 if ( ! is_array( $fields ) || empty( $fields ) || ! array_is_list( $fields ) ) { 701 _doing_it_wrong( 702 $method, 703 __( 'The fields must be a list of non-empty field definitions.' ), 704 '7.2.0' 705 ); 706 return false; 707 } 708 709 if ( null !== $script_module && ( ! is_string( $script_module ) || '' === $script_module ) ) { 710 _doing_it_wrong( 711 $method, 712 __( 'The script module must be the id of a script module.' ), 713 '7.2.0' 714 ); 715 return false; 716 } 717 718 return true; 719 } 720 721 /** 722 * Checks that the entity kind and name are non-empty strings, reporting 723 * them with _doing_it_wrong() otherwise. 724 * 725 * @since 7.2.0 726 * 727 * @param string $method The calling method, for the notice. 728 * @param mixed $kind The entity kind. 729 * @param mixed $name The entity name. 730 * @return bool Whether both are valid. 731 */ 732 protected function validate_entity( $method, $kind, $name ) { 733 foreach ( array( $kind, $name ) as $argument ) { 734 if ( ! is_string( $argument ) || '' === $argument ) { 735 _doing_it_wrong( 736 $method, 737 __( 'The entity kind and the entity name must be non-empty strings.' ), 738 '7.2.0' 739 ); 740 return false; 741 } 742 } 743 return true; 744 } 745 746 /** 747 * Drops the definitions that are not an array with a non-empty string 748 * `id`, reporting their positions in the call with _doing_it_wrong(): 749 * like a duplicated field, an invalid definition does not cost the call 750 * its other fields. 751 * 752 * @since 7.2.0 753 * 754 * @param string $method The calling method, for the notice. 755 * @param string $kind The entity kind. 756 * @param string $name The entity name. 757 * @param array[] $fields The list of field definitions. 758 * @return array[] The valid definitions, in the order of the call. 759 */ 760 protected function skip_invalid_definitions( $method, $kind, $name, $fields ) { 761 $valid = array(); 762 $invalid = array(); 763 foreach ( $fields as $position => $field ) { 764 if ( is_array( $field ) && isset( $field['id'] ) && is_string( $field['id'] ) && '' !== $field['id'] ) { 765 $valid[] = $field; 766 } else { 767 $invalid[] = '#' . ( $position + 1 ); 768 } 769 } 770 771 if ( $invalid ) { 772 _doing_it_wrong( 773 $method, 774 sprintf( 775 /* 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 */ 776 __( '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.' ), 777 $kind, 778 $name, 779 implode( ', ', $invalid ), 780 '<code>id</code>' 781 ), 782 '7.2.0' 783 ); 784 } 785 786 return $valid; 787 } 788 789 /** 790 * Drops the ids that are not a non-empty string, reporting their 791 * positions in the call with _doing_it_wrong(): like an invalid 792 * definition, an invalid id does not cost the call its other ids. 793 * 794 * @since 7.2.0 795 * 796 * @param string $method The calling method, for the notice. 797 * @param string $kind The entity kind. 798 * @param string $name The entity name. 799 * @param array $ids The ids of the fields. 800 * @return string[] The valid ids, in the order of the call. 801 */ 802 protected function skip_invalid_ids( $method, $kind, $name, $ids ) { 803 $valid = array(); 804 $invalid = array(); 805 foreach ( array_values( $ids ) as $position => $id ) { 806 if ( is_string( $id ) && '' !== $id ) { 807 $valid[] = $id; 808 } else { 809 $invalid[] = '#' . ( $position + 1 ); 810 } 811 } 812 813 if ( $invalid ) { 814 _doing_it_wrong( 815 $method, 816 sprintf( 817 /* translators: 1: Entity kind, e.g. postType. 2: Entity name, e.g. page. 3: Comma-separated list of positions, e.g. #2, #4. */ 818 __( '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.' ), 819 $kind, 820 $name, 821 implode( ', ', $invalid ) 822 ), 823 '7.2.0' 824 ); 825 } 826 827 return $valid; 828 } 829 830 /** 831 * Reads and validates a field collection, see register_collection() for 832 * its format. 833 * 834 * An invalid configuration is reported with _doing_it_wrong(): the 835 * collection is skipped. 836 * 837 * @since 7.2.0 838 * 839 * @param string $method The calling method, for the notice. 840 * @param string $directory The directory of the collection. 841 * @return array|null The configuration of the collection, with the `module` 842 * key set, its valid `fields` as a list, and `skipped` 843 * telling whether a `field.php` was skipped, see 844 * read_collection_fields(); or null when the 845 * configuration is invalid. 846 */ 847 protected function get_collection( $method, $directory ) { 848 $slug = basename( $directory ); 849 $file = $directory . '/index.php'; 850 $config = is_file( $file ) ? self::require_file( $file ) : null; 851 $error = null; 852 853 if ( ! is_array( $config ) ) { 854 $error = sprintf( 855 /* translators: %s: index.php */ 856 __( 'The %s of a field collection must return an array.' ), 857 '<code>index.php</code>' 858 ); 859 } elseif ( ! isset( $config['origin'] ) || ! is_string( $config['origin'] ) || '' === $config['origin'] ) { 860 $error = sprintf( 861 /* translators: %s: origin */ 862 __( 'The %s of a field collection must be a non-empty string.' ), 863 '<code>origin</code>' 864 ); 865 } elseif ( ! isset( $config['kind'] ) || ! is_string( $config['kind'] ) || '' === $config['kind'] ) { 866 $error = sprintf( 867 /* translators: %s: kind */ 868 __( 'The %s of a field collection must be a non-empty string.' ), 869 '<code>kind</code>' 870 ); 871 } elseif ( ! isset( $config['name'] ) || ! is_string( $config['name'] ) || '' === $config['name'] ) { 872 $error = sprintf( 873 /* translators: %s: name */ 874 __( 'The %s of a field collection must be a non-empty string.' ), 875 '<code>name</code>' 876 ); 877 } elseif ( isset( $config['module'] ) && ( ! is_string( $config['module'] ) || '' === $config['module'] ) ) { 878 $error = sprintf( 879 /* translators: %s: module */ 880 __( 'The %s of a field collection must be the id of a script module.' ), 881 '<code>module</code>' 882 ); 883 } 884 885 if ( null !== $error ) { 886 _doing_it_wrong( 887 $method, 888 /* translators: 1: The name of a field collection. 2: The error. */ 889 sprintf( __( 'The field collection "%1$s" is skipped. %2$s' ), $slug, $error ), 890 '7.2.0' 891 ); 892 return null; 893 } 894 895 $read = $this->read_collection_fields( $method, $directory ); 896 897 return array_merge( 898 $config, 899 array( 900 'module' => $config['module'] ?? null, 901 'fields' => array_values( $read['fields'] ), 902 'skipped' => $read['skipped'], 903 ) 904 ); 905 } 906 907 /** 908 * Reads the fields of a field collection, see get_collection_fields(). 909 * 910 * The id of a field is used as a key, so it is checked before: a 911 * `field.php` that does not return an array with a non-empty string id 912 * is reported with _doing_it_wrong() and skipped, and so is one whose 913 * id an earlier folder of the collection took. The rest of the fields 914 * are read. 915 * 916 * @since 7.2.0 917 * 918 * @param string $method The calling method, for the notice. 919 * @param string $directory The directory of the collection. 920 * @return array { 921 * The fields of the collection. 922 * 923 * @type array<string, array> $fields The valid fields, keyed by id, 924 * in the alphabetical order of 925 * their folders. 926 * @type bool $skipped Whether a `field.php` was skipped. 927 * } 928 */ 929 protected function read_collection_fields( $method, $directory ) { 930 $files = glob( $directory . '/*/field.php' ); 931 if ( empty( $files ) ) { 932 return array( 933 'fields' => array(), 934 'skipped' => false, 935 ); 936 } 937 sort( $files, SORT_STRING ); 938 939 $fields = array(); 940 $invalid = array(); 941 $duplicated = array(); 942 foreach ( $files as $file ) { 943 $folder = basename( dirname( $file ) ); 944 $field = self::require_file( $file ); 945 if ( ! is_array( $field ) ) { 946 $invalid[] = $folder; 947 continue; 948 } 949 950 // The id comes first, like in the fields registered directly. 951 $field = array_merge( array( 'id' => $folder ), $field ); 952 $id = $field['id']; 953 if ( ! is_string( $id ) || '' === $id ) { 954 $invalid[] = $folder; 955 } elseif ( isset( $fields[ $id ] ) ) { 956 $duplicated[] = $folder; 957 } else { 958 $fields[ $id ] = $field; 959 } 960 } 961 962 $collection = basename( $directory ); 963 if ( $invalid ) { 964 _doing_it_wrong( 965 $method, 966 sprintf( 967 /* translators: 1: The name of a field collection. 2: Comma-separated list of folder names. 3: field.php 4: id */ 968 __( '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.' ), 969 $collection, 970 implode( ', ', $invalid ), 971 '<code>field.php</code>', 972 '<code>id</code>' 973 ), 974 '7.2.0' 975 ); 976 } 977 978 if ( $duplicated ) { 979 _doing_it_wrong( 980 $method, 981 sprintf( 982 /* translators: 1: The name of a field collection. 2: Comma-separated list of folder names. */ 983 __( '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.' ), 984 $collection, 985 implode( ', ', $duplicated ) 986 ), 987 '7.2.0' 988 ); 989 } 990 991 return array( 992 'fields' => $fields, 993 'skipped' => $invalid || $duplicated, 994 ); 995 } 996 997 /** 998 * Requires a file without sharing the variables of the caller: this is 999 * about variable scope. 1000 * 1001 * @since 7.2.0 1002 * 1003 * @param string $file The path of the file. 1004 * @return mixed The return value of the file. 1005 */ 1006 protected static function require_file( $file ) { 1007 return require $file; 1008 } 1009 1010 /** 1011 * Applies a script module to fields of an entity, after the fields it 1012 * already applies to. 1013 * 1014 * @since 7.2.0 1015 * 1016 * @param string $kind The entity kind. 1017 * @param string $name The entity name. 1018 * @param string[] $ids The ids of the fields. 1019 * @param string|null $script_module The id of the script module, if any. 1020 */ 1021 protected function add_field_module( $kind, $name, $ids, $script_module ) { 1022 if ( null === $script_module ) { 1023 return; 1024 } 1025 1026 $module_ids = $this->field_modules[ $kind ][ $name ][ $script_module ] ?? array(); 1027 foreach ( $ids as $id ) { 1028 if ( ! in_array( $id, $module_ids, true ) ) { 1029 $module_ids[] = $id; 1030 } 1031 } 1032 $this->field_modules[ $kind ][ $name ][ $script_module ] = $module_ids; 1033 } 1034 1035 /** 1036 * Forgets an entity left without fields or script modules, and its kind 1037 * once no entity of the kind is left, so the registry only lists 1038 * entities that have some. 1039 * 1040 * @since 7.2.0 1041 * 1042 * @param string $kind The entity kind. 1043 * @param string $name The entity name. 1044 */ 1045 protected function forget_empty_entity( $kind, $name ) { 1046 if ( empty( $this->fields[ $kind ][ $name ] ) ) { 1047 unset( $this->fields[ $kind ][ $name ] ); 1048 } 1049 if ( empty( $this->fields[ $kind ] ) ) { 1050 unset( $this->fields[ $kind ] ); 1051 } 1052 if ( empty( $this->field_modules[ $kind ][ $name ] ) ) { 1053 unset( $this->field_modules[ $kind ][ $name ] ); 1054 } 1055 if ( empty( $this->field_modules[ $kind ] ) ) { 1056 unset( $this->field_modules[ $kind ] ); 1057 } 1058 } 1059 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Mon Nov 9 08:20:01 2026 | Cross-referenced by PHPXref |