[ 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<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  }


Generated : Tue Oct 20 08:20:34 2026 Cross-referenced by PHPXref