[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-entity-fields-registry.php (source)

   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  }


Generated : Mon Nov 9 08:20:01 2026 Cross-referenced by PHPXref