Make WordPress Core

Changeset 64686


Ignore:
Timestamp:
10/23/2026 07:31:51 AM (7 hours ago)
Author:
oandregal
Message:

Editor: Fix edge cases in the entity actions and fields APIs.

_wp_preload_entity_actions() and _wp_preload_entity_fields() assumed
$paths was an array, so the editor crashed when an earlier
block_editor_rest_api_preload_paths callback returned null, false,
a string or an object. Empty values are now turned into an array so the
entity paths are still added. Other non-array values, and contexts that
are not a WP_Block_Editor_Context, are returned unchanged. $context
now defaults to null, so calling either function with one argument no
longer throws an ArgumentCountError.

Numeric script module ids are now handled as strings. PHP turned them into
integer array keys, so wp_get_registered_entity_field_modules() and
wp_get_registered_entity_action_modules() could miss them in a strict
comparison and array_merge() could renumber them. Both functions now
return a list of entries holding the module id and the ids of the fields
or actions it applies to. This is the shape the REST controllers already
returned, and they now use these results directly. The entity fields
endpoint now returns module ids as strings, matching its schema.

A field registered with the id '0' is no longer rejected as empty.

The code duplicated between the two APIs now lives in a new
entity-utils.php file:

  • _wp_add_entity_preload_paths() builds the preload paths for both preload callbacks.
  • _wp_add_entity_modules_to_editor_script() adds the script modules to the editor script for both admin_footer callbacks. It takes a callable rather than the modules, so the registries are still only read on pages that load the editor script.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/14716.

Props oandregal, jorgefilipecosta.
See #66229, #66343.

Location:
trunk
Files:
1 added
15 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-entity-actions-registry.php

    r64601 r64686  
    405405         * @param string $kind The entity kind (e.g. `postType`).
    406406         * @param string $name The entity name (e.g. `page`).
    407          * @return array<string, string[]> The ids of the actions each module
    408          *                                 applies to, keyed by module id, in
    409          *                                 registration order. Empty when the
    410          *                                 entity is invalid.
     407         * @return array[] {
     408         *     The script modules, in registration order. Empty when the entity
     409         *     is invalid.
     410         *
     411         *     @type array ...$0 {
     412         *         @type string   $id      The id of the script module.
     413         *         @type string[] $actions The ids of the actions the module
     414         *                                 applies to, in registration order.
     415         *     }
     416         * }
    411417         */
    412418        public function get_registered_action_modules( $kind, $name ) {
    … …  
    415421                }
    416422                $this->initialize();
    417                 return $this->action_modules[ $kind ][ $name ] ?? array();
     423                $modules = array();
     424                foreach ( $this->action_modules[ $kind ][ $name ] ?? array() as $module => $action_ids ) {
     425                        $modules[] = array(
     426                                // A numeric module id is an integer as an array key.
     427                                'id'      => (string) $module,
     428                                'actions' => $action_ids,
     429                        );
     430                }
     431                return $modules;
    418432        }
    419433
  • trunk/src/wp-includes/class-wp-entity-fields-registry.php

    r64528 r64686  
    478478         * @param string $kind The entity kind (e.g. `postType`).
    479479         * @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.
     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         * }
    483489         */
    484490        public function get_registered_field_modules( $kind, $name ) {
    485491                $this->initialize();
    486                 return $this->field_modules[ $kind ][ $name ] ?? array();
     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;
    487501        }
    488502
    … …  
    498512        public function get_all_registered_field_modules() {
    499513                $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                 */
    500524                return array_map(
    501525                        static function ( $entities ) {
    502                                 return array_map( 'array_keys', $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                                );
    503533                        },
    504534                        $this->field_modules
    … …  
    732762                $invalid = array();
    733763                foreach ( $fields as $position => $field ) {
    734                         if ( is_array( $field ) && ! empty( $field['id'] ) && is_string( $field['id'] ) ) {
     764                        if ( is_array( $field ) && isset( $field['id'] ) && is_string( $field['id'] ) && '' !== $field['id'] ) {
    735765                                $valid[] = $field;
    736766                        } else {
  • trunk/src/wp-includes/entity-actions.php

    r64595 r64686  
    8181
    8282/**
    83  * Returns the ids of the script modules registered for the given entity.
     83 * Returns the script modules registered for the given entity.
    8484 *
    8585 * @since 7.2.0
    … …  
    8787 * @param string $kind The entity kind (e.g. `postType`).
    8888 * @param string $name The entity name (e.g. `page`).
    89  * @return array<string, string[]> The ids of the actions each module
    90  *                                 applies to, keyed by module id, in
    91  *                                 registration order. Empty until the
    92  *                                 `init` action has completed: a read
    93  *                                 before then is refused.
     89 * @return array[] {
     90 *     The script modules, in registration order. Empty until the `init`
     91 *     action has completed: a read before then is refused.
     92 *
     93 *     @type array ...$0 {
     94 *         @type string   $id      The id of the script module.
     95 *         @type string[] $actions The ids of the actions the module applies
     96 *                                 to, in registration order.
     97 *     }
     98 * }
    9499 */
    95100function wp_get_registered_entity_action_modules( $kind, $name ) {
    … …  
    115120/**
    116121 * Declares the script modules of the entity actions as dependencies of the
    117  * editor script.
     122 * editor script, see _wp_add_entity_modules_to_editor_script().
    118123 *
    119  * The editor imports the script modules of an entity on demand, with a dynamic
    120  * `import()` the browser resolves through the import map of the page. Listing
    121  * the modules as dynamic `module_dependencies` of the `wp-editor` script adds
    122  * them to the import map of every page that loads the editor script: the post
    123  * editor, the site editor, and the pages booted by `@wordpress/boot`, whose
    124  * prerequisites depend on it. A dynamic dependency is only listed in the
    125  * import map; the module is fetched when it is imported.
    126  *
    127  * Reading the registry fires `wp_entity_actions_api_init`, so the actions and their
    128  * script modules are registered on demand. It only reads the registry on the
    129  * pages that load the editor script, not on every admin request (heartbeat,
    130  * Ajax, other screens). It runs on `admin_footer`, the last action before the
    131  * import map is printed that fires both in the admin template and in the pages
    132  * rendered outside it, which print the import map themselves. By then `init`
    133  * has completed, so the post types and supports the actions derive from are
    134  * final.
    135  * The registry only accepts registrations while the action fires, so the
    136  * modules declared here are the modules of every action the page can show.
     124 * The actions are loaded by `usePostActions()` of the editor script, so every
     125 * screen showing them loads it.
    137126 *
    138127 * The `admin_footer` action passes it no argument, so the global registry is
    … …  
    140129 *
    141130 * @since 7.2.0
     131 * @access private
    142132 *
    143133 * @param WP_Scripts|null $scripts The scripts registry. Defaults to the global one.
    144134 */
    145135function _wp_add_entity_action_modules_to_editor_script( $scripts = null ) {
    146         if ( ! $scripts instanceof WP_Scripts ) {
    147                 $scripts = wp_scripts();
    148         }
    149 
    150         // The actions are loaded by `usePostActions()` of the editor script, so
    151         // every screen showing them loads it.
    152         if ( ! $scripts->query( 'wp-editor', 'enqueued' ) && ! $scripts->query( 'wp-editor', 'done' ) ) {
    153                 return;
    154         }
    155 
    156         $kinds = wp_get_all_registered_entity_action_modules();
    157         if ( empty( $kinds ) ) {
    158                 return;
    159         }
    160 
    161         $dependencies = $scripts->get_data( 'wp-editor', 'module_dependencies' );
    162         $dependencies = is_array( $dependencies ) ? $dependencies : array();
    163 
    164         $declared = array();
    165         foreach ( $dependencies as $dependency ) {
    166                 $declared[] = is_array( $dependency ) ? $dependency['id'] : $dependency;
    167         }
    168 
    169         $added = false;
    170         foreach ( $kinds as $entities ) {
    171                 foreach ( $entities as $modules ) {
    172                         foreach ( $modules as $module ) {
    173                                 if ( ! in_array( $module, $declared, true ) ) {
    174                                         $dependencies[] = array(
    175                                                 'id'     => $module,
    176                                                 'import' => 'dynamic',
    177                                         );
    178                                         $declared[]     = $module;
    179                                         $added          = true;
    180                                 }
    181                         }
    182                 }
    183         }
    184 
    185         if ( $added ) {
    186                 // `add_data()` replaces the value, hence the merge above.
    187                 $scripts->add_data( 'wp-editor', 'module_dependencies', $dependencies );
    188         }
     136        _wp_add_entity_modules_to_editor_script( $scripts, 'wp_get_all_registered_entity_action_modules' );
    189137}
    190138
    … …  
    202150 *
    203151 * @since 7.2.0
     152 * @access private
    204153 *
    205  * @param array                  $paths   REST API paths to preload.
    206  * @param WP_Block_Editor_Context $context Block editor context.
    207  * @return array Filtered preload paths.
     154 * @param mixed $paths   REST API paths to preload.
     155 * @param mixed $context Optional. Block editor context. Default null.
     156 * @return mixed Filtered preload paths. See _wp_add_entity_preload_paths().
    208157 */
    209 function _wp_preload_entity_actions( $paths, $context ) {
    210         $post_types = array();
    211         if ( 'core/edit-post' === $context->name && isset( $context->post ) ) {
    212                 $post_types[] = $context->post->post_type;
    213         } elseif ( 'core/edit-site' === $context->name ) {
    214                 if ( isset( $context->post ) ) {
    215                         $post_types[] = $context->post->post_type;
    216                 }
    217                 $post_types = array_merge( $post_types, _wp_get_site_editor_screen_post_types() );
    218         }
    219 
    220         foreach ( array_unique( $post_types ) as $post_type ) {
    221                 if ( ! post_type_exists( $post_type ) ) {
    222                         continue;
    223                 }
    224                 $paths[] = add_query_arg(
    225                         array(
    226                                 'kind' => 'postType',
    227                                 'name' => $post_type,
    228                         ),
    229                         '/wp/v2/entity-actions'
    230                 );
    231         }
    232 
    233         return $paths;
     158function _wp_preload_entity_actions( $paths, $context = null ) {
     159        return _wp_add_entity_preload_paths( $paths, $context, '/wp/v2/entity-actions' );
    234160}
  • trunk/src/wp-includes/entity-fields.php

    r64595 r64686  
    112112
    113113/**
    114  * Returns the ids of the script modules registered for the given entity.
     114 * Returns the script modules registered for the given entity.
    115115 *
    116116 * @since 7.2.0
    … …  
    118118 * @param string $kind The entity kind (e.g. `postType`).
    119119 * @param string $name The entity name (e.g. `page`).
    120  * @return array<string, string[]> The ids of the fields each module applies
    121  *                                 to, keyed by module id, in registration
    122  *                                 order. Empty until the `init` action has
    123  *                                 completed: a read before then is refused.
     120 * @return array[] {
     121 *     The script modules, in registration order. Empty until the `init`
     122 *     action has completed: a read before then is refused.
     123 *
     124 *     @type array ...$0 {
     125 *         @type string   $id     The id of the script module.
     126 *         @type string[] $fields The ids of the fields the module applies to,
     127 *                                in registration order.
     128 *     }
     129 * }
    124130 */
    125131function wp_get_registered_entity_field_modules( $kind, $name ) {
    … …  
    145151/**
    146152 * Declares the script modules of the entity fields as dependencies of the
    147  * editor script.
    148  *
    149  * The editor imports the script modules of an entity on demand, with a dynamic
    150  * `import()` the browser resolves through the import map of the page. Listing
    151  * the modules as dynamic `module_dependencies` of the `wp-editor` script adds
    152  * them to the import map of every page that loads the editor script: the post
    153  * editor, the site editor, and the pages booted by `@wordpress/boot`, whose
    154  * prerequisites depend on it. A dynamic dependency is only listed in the
    155  * import map; the module is fetched when it is imported.
    156  *
    157  * Reading the registry fires `wp_entity_fields_api_init`, so the fields and their
    158  * script modules are registered on demand. It only reads the registry on the
    159  * pages that load the editor script, not on every admin request (heartbeat,
    160  * Ajax, other screens). It runs on `admin_footer`, the last action before the
    161  * import map is printed that fires both in the admin template and in the pages
    162  * rendered outside it, which print the import map themselves. By then `init`
    163  * has completed, so the supports the default fields derive from are final.
    164  * The registry only accepts registrations while the action fires, so the
    165  * modules declared here are the modules of every field the page can show.
     153 * editor script, see _wp_add_entity_modules_to_editor_script().
     154 *
     155 * The fields are loaded by the editor package, see
     156 * packages/editor/src/dataviews/store/private-resolvers.ts in Gutenberg, which
     157 * means any screen that wants to use this mechanism needs to load the editor
     158 * script.
    166159 *
    167160 * It is hooked to `admin_footer` without arguments, so the global scripts
    … …  
    175168 */
    176169function _wp_add_entity_field_modules_to_editor_script( $scripts = null ) {
    177         if ( ! $scripts instanceof WP_Scripts ) {
    178                 $scripts = wp_scripts();
    179         }
    180 
    181         /*
    182          * The fields are loaded by the editor package, see
    183          * packages/editor/src/dataviews/store/private-resolvers.ts in Gutenberg,
    184          * which means any screen that wants to use this mechanism needs to load
    185          * the editor script.
    186          */
    187         if ( ! $scripts->query( 'wp-editor', 'enqueued' ) && ! $scripts->query( 'wp-editor', 'done' ) ) {
    188                 return;
    189         }
    190 
    191         $kinds = wp_get_all_registered_entity_field_modules();
    192         if ( empty( $kinds ) ) {
    193                 return;
    194         }
    195 
    196         $dependencies = $scripts->get_data( 'wp-editor', 'module_dependencies' );
    197         $dependencies = is_array( $dependencies ) ? $dependencies : array();
    198 
    199         $declared = array();
    200         foreach ( $dependencies as $dependency ) {
    201                 $declared[] = is_array( $dependency ) ? $dependency['id'] : $dependency;
    202         }
    203 
    204         $added = false;
    205         foreach ( $kinds as $entities ) {
    206                 foreach ( $entities as $modules ) {
    207                         foreach ( $modules as $module ) {
    208                                 if ( ! in_array( $module, $declared, true ) ) {
    209                                         $dependencies[] = array(
    210                                                 'id'     => $module,
    211                                                 'import' => 'dynamic',
    212                                         );
    213                                         $declared[]     = $module;
    214                                         $added          = true;
    215                                 }
    216                         }
    217                 }
    218         }
    219 
    220         if ( $added ) {
    221                 // `add_data()` replaces the value, hence the merge above.
    222                 $scripts->add_data( 'wp-editor', 'module_dependencies', $dependencies );
    223         }
     170        _wp_add_entity_modules_to_editor_script( $scripts, 'wp_get_all_registered_entity_field_modules' );
    224171}
    225172
    … …  
    254201function wp_register_entity_field_collection( $directory ) {
    255202        return WP_Entity_Fields_Registry::get_instance()->register_collection( $directory );
    256 }
    257 
    258 /**
    259  * Returns the paths that serve the fields of the given post types, skipping
    260  * the post types that are not registered.
    261  *
    262  * A path must match the request of the `getFieldsConfig` core data resolver,
    263  * see packages/core-data/src/resolvers.js in Gutenberg.
    264  *
    265  * @since 7.2.0
    266  * @access private
    267  *
    268  * @param string[] $post_types The post types.
    269  * @return string[] One path of the `/wp/v2/entity-fields` route per post type,
    270  *                  in the order the post types are given, without duplicates.
    271  */
    272 function _wp_get_post_type_fields_preload_paths( $post_types ) {
    273         $paths = array();
    274         foreach ( array_unique( $post_types ) as $post_type ) {
    275                 if ( ! post_type_exists( $post_type ) ) {
    276                         continue;
    277                 }
    278                 $paths[] = add_query_arg(
    279                         array(
    280                                 'kind' => 'postType',
    281                                 'name' => $post_type,
    282                         ),
    283                         '/wp/v2/entity-fields'
    284                 );
    285         }
    286 
    287         return $paths;
    288 }
    289 
    290 /**
    291  * Returns the post types whose items the screen of the site editor being
    292  * loaded lists or edits, from its `p` query arg.
    293  *
    294  * The screens are the routes in
    295  * packages/edit-site/src/components/site-editor-routes in Gutenberg: the
    296  * lists and the editors of pages, templates, patterns, and template parts.
    297  * The patterns screen lists both patterns and template parts.
    298  *
    299  * @since 7.2.0
    300  * @access private
    301  *
    302  * @return string[] The post types, empty for any other screen.
    303  */
    304 function _wp_get_site_editor_screen_post_types() {
    305         // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only picks what to preload.
    306         $path     = isset( $_GET['p'] ) && is_string( $_GET['p'] ) ? sanitize_text_field( wp_unslash( $_GET['p'] ) ) : '';
    307         $segments = explode( '/', trim( $path, '/' ) );
    308 
    309         switch ( $segments[0] ) {
    310                 case 'page':
    311                         return array( 'page' );
    312                 case 'template':
    313                 case 'wp_template':
    314                         return array( 'wp_template' );
    315                 case 'pattern':
    316                         return array( 'wp_block', 'wp_template_part' );
    317                 case 'wp_block':
    318                         return array( 'wp_block' );
    319                 case 'wp_template_part':
    320                         return array( 'wp_template_part' );
    321         }
    322         return array();
    323203}
    324204
    … …  
    330210 * post editor reads those of the edited post in the post summary of its
    331211 * inspector. The site editor reads those of the edited post, if any, and of
    332  * the post types of the screen it opens on.
     212 * the post types of the screen it opens on. A path must match the request of
     213 * the `getFieldsConfig` core data resolver, see
     214 * packages/core-data/src/resolvers.js in Gutenberg.
    333215 *
    334216 * @since 7.2.0
    335217 * @access private
    336218 *
    337  * @param array                   $paths   REST API paths to preload.
    338  * @param WP_Block_Editor_Context $context Block editor context.
    339  * @return array Filtered preload paths.
    340  */
    341 function _wp_preload_entity_fields( $paths, $context ) {
    342         $post_types = array();
    343         if ( 'core/edit-post' === $context->name && isset( $context->post ) ) {
    344                 $post_types[] = $context->post->post_type;
    345         } elseif ( 'core/edit-site' === $context->name ) {
    346                 if ( isset( $context->post ) ) {
    347                         $post_types[] = $context->post->post_type;
    348                 }
    349                 $post_types = array_merge( $post_types, _wp_get_site_editor_screen_post_types() );
    350         }
    351 
    352         return array_merge(
    353                 $paths,
    354                 _wp_get_post_type_fields_preload_paths( $post_types )
    355         );
    356 }
     219 * @param mixed $paths   REST API paths to preload.
     220 * @param mixed $context Optional. Block editor context. Default null.
     221 * @return mixed Filtered preload paths. See _wp_add_entity_preload_paths().
     222 */
     223function _wp_preload_entity_fields( $paths, $context = null ) {
     224        return _wp_add_entity_preload_paths( $paths, $context, '/wp/v2/entity-fields' );
     225}
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-entity-actions-controller.php

    r64517 r64686  
    198198                $name = $request->get_param( 'name' );
    199199
    200                 $script_modules = array();
    201                 foreach ( wp_get_registered_entity_action_modules( $kind, $name ) as $module => $action_ids ) {
    202                         $script_modules[] = array(
    203                                 // A numeric module id is an integer as an array key.
    204                                 'id'      => (string) $module,
    205                                 'actions' => array_values( $action_ids ),
    206                         );
    207                 }
    208 
    209200                $response = array(
    210201                        'kind'           => $kind,
    211202                        'name'           => $name,
    212203                        'actions'        => wp_get_registered_entity_actions( $kind, $name ),
    213                         'script_modules' => $script_modules,
     204                        'script_modules' => wp_get_registered_entity_action_modules( $kind, $name ),
    214205                );
    215206
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-entity-fields-controller.php

    r64602 r64686  
    160160                }
    161161
    162                 $script_modules = array();
    163                 foreach ( wp_get_registered_entity_field_modules( $kind, $name ) as $module => $field_ids ) {
    164                         $script_modules[] = array(
    165                                 'id'     => $module,
    166                                 'fields' => array_values( $field_ids ),
    167                         );
    168                 }
    169 
    170162                $response = array(
    171163                        'kind'           => $kind,
    172164                        'name'           => $name,
    173165                        'fields'         => $fields,
    174                         'script_modules' => $script_modules,
     166                        'script_modules' => wp_get_registered_entity_field_modules( $kind, $name ),
    175167                );
    176168
  • trunk/src/wp-settings.php

    r64581 r64686  
    207207require ABSPATH . WPINC . '/class-wp-view-config-data.php';
    208208require ABSPATH . WPINC . '/view-config.php';
     209require ABSPATH . WPINC . '/entity-utils.php';
    209210require ABSPATH . WPINC . '/class-wp-entity-actions-registry.php';
    210211require ABSPATH . WPINC . '/entity-actions.php';
  • trunk/tests/phpunit/tests/entity-actions.php

    r64601 r64686  
    99 *
    1010 * @covers ::_wp_add_entity_action_modules_to_editor_script
     11 * @covers ::_wp_add_entity_modules_to_editor_script
     12 * @covers ::_wp_add_entity_preload_paths
    1113 * @covers ::_wp_preload_entity_actions
    1214 * @covers ::wp_get_all_registered_entity_action_modules
    … …  
    672674                        'The removed action is gone, and the replaced one has the new definition and origin.'
    673675                );
    674                 $this->assertSame( array( 'core/defaults' => array( 'move-to-trash' ) ), $modules, 'The replaced action keeps the script module of the default.' );
     676                $this->assertSame(
     677                        array(
     678                                array(
     679                                        'id'      => 'core/defaults',
     680                                        'actions' => array( 'move-to-trash' ),
     681                                ),
     682                        ),
     683                        $modules,
     684                        'The replaced action keeps the script module of the default.'
     685                );
    675686        }
    676687
    … …  
    718729                $this->assertSame( 'Rename…', $actions['rename-post']['label'], 'The registered action is left untouched.' );
    719730                $this->assertSame( 'core', $actions['rename-post']['origin']['registeredBy'] );
    720                 $this->assertSame( array( 'feature' ), wp_get_registered_entity_action_modules( 'postType', 'page' )['plugin/actions'], 'The script module only applies to the actions registered.' );
     731                $this->assertSame( array( 'feature' ), array_column( wp_get_registered_entity_action_modules( 'postType', 'page' ), 'actions', 'id' )['plugin/actions'], 'The script module only applies to the actions registered.' );
    721732                $this->assertCount( 1, $reported );
    722733                $this->assertStringContainsString( 'postType "page"', $reported[0], 'The notice names the entity.' );
    … …  
    814825                $this->assertSame(
    815826                        array(
    816                                 'core/defaults'        => array( 'rename-post', 'move-to-trash' ),
    817                                 'plugin/actions'       => array( 'feature', 'archive' ),
    818                                 'other-plugin/actions' => array( 'share' ),
     827                                array(
     828                                        'id'      => 'core/defaults',
     829                                        'actions' => array( 'rename-post', 'move-to-trash' ),
     830                                ),
     831                                array(
     832                                        'id'      => 'plugin/actions',
     833                                        'actions' => array( 'feature', 'archive' ),
     834                                ),
     835                                array(
     836                                        'id'      => 'other-plugin/actions',
     837                                        'actions' => array( 'share' ),
     838                                ),
    819839                        ),
    820840                        wp_get_registered_entity_action_modules( 'postType', 'page' ),
    … …  
    845865                $this->assertSame( array(), wp_get_registered_entity_actions( 'a/b', 'c' ) );
    846866                $this->assertSame( array( 'feature', 'archive' ), array_column( wp_get_registered_entity_actions( 'a', 'b/c' ), 'id' ), 'The second entity keeps its actions.' );
    847                 $this->assertSame( array( 'plugin/second' => array( 'feature', 'archive' ) ), wp_get_registered_entity_action_modules( 'a', 'b/c' ), 'The second entity keeps only its own module.' );
     867                $this->assertSame(
     868                        array(
     869                                array(
     870                                        'id'      => 'plugin/second',
     871                                        'actions' => array( 'feature', 'archive' ),
     872                                ),
     873                        ),
     874                        wp_get_registered_entity_action_modules( 'a', 'b/c' ),
     875                        'The second entity keeps only its own module.'
     876                );
    848877                $this->assertSame( array( 'b/c' => array( 'plugin/second' ) ), wp_get_all_registered_entity_action_modules()['a'], 'Only the second entity has modules under its kind.' );
    849878                $this->assertArrayNotHasKey( 'a/b', wp_get_all_registered_entity_action_modules(), 'The kind of the first entity, emptied, is forgotten.' );
    … …  
    882911                        'The actions unregistered by id are returned in registration order.'
    883912                );
    884                 $this->assertSame( array( 'plugin/first' => array( 'b' ) ), $modules['by_id'], 'The unregistered actions leave their modules, and an emptied module is forgotten.' );
     913                $this->assertSame(
     914                        array(
     915                                array(
     916                                        'id'      => 'plugin/first',
     917                                        'actions' => array( 'b' ),
     918                                ),
     919                        ),
     920                        $modules['by_id'],
     921                        'The unregistered actions leave their modules, and an emptied module is forgotten.'
     922                );
    885923                $this->assertSame( array(), $unregistered['missing'], 'Nothing is returned when no action is registered.' );
    886924                $this->assertSame(
    … …  
    9891027                $this->assertCount( 3, $reported );
    9901028                $this->assertNotContains( 'feature', array_column( wp_get_registered_entity_actions( 'postType', 'page' ), 'id' ) );
    991                 $this->assertArrayNotHasKey( 'plugin/actions', wp_get_registered_entity_action_modules( 'postType', 'page' ) );
     1029                $this->assertNotContains( 'plugin/actions', array_column( wp_get_registered_entity_action_modules( 'postType', 'page' ), 'id' ) );
    9921030        }
    9931031
    … …  
    10851123
    10861124                $this->assertSame( array( 'core/defaults', '123' ), wp_get_all_registered_entity_action_modules()['postType']['page'] );
     1125                $this->assertSame( array( 'core/defaults', '123' ), array_column( wp_get_registered_entity_action_modules( 'postType', 'page' ), 'id' ) );
    10871126        }
    10881127
    … …  
    11031142                $this->assertSame( $fired, did_action( 'wp_entity_actions_api_init' ), 'Refusing does not fire the action.' );
    11041143                $this->assertNotContains( 'feature', array_column( wp_get_registered_entity_actions( 'postType', 'page' ), 'id' ) );
    1105                 $this->assertArrayNotHasKey( 'plugin/feature', wp_get_registered_entity_action_modules( 'postType', 'page' ) );
     1144                $this->assertNotContains( 'plugin/feature', array_column( wp_get_registered_entity_action_modules( 'postType', 'page' ), 'id' ) );
    11061145        }
    11071146
  • trunk/tests/phpunit/tests/entity-actions/registerCoreEntityActions.php

    r64566 r64686  
    165165                                self::PAGE               => self::PAGE_ACTIONS,
    166166                        ),
    167                         wp_get_registered_entity_action_modules( 'postType', 'page' ),
     167                        array_column( wp_get_registered_entity_action_modules( 'postType', 'page' ), 'actions', 'id' ),
    168168                        'Each action of pages is registered with the module of its folder.'
    169169                );
    … …  
    206206                                self::WP_BLOCK           => array( 'duplicate-pattern', 'export-pattern' ),
    207207                        ),
    208                         wp_get_registered_entity_action_modules( 'postType', 'wp_block' ),
     208                        array_column( wp_get_registered_entity_action_modules( 'postType', 'wp_block' ), 'actions', 'id' ),
    209209                        'The actions of patterns come from the `wp_block` module.'
    210210                );
    … …  
    244244                $this->assertSame(
    245245                        array( 'duplicate-template-part' ),
    246                         wp_get_registered_entity_action_modules( 'postType', 'wp_template_part' )[ self::WP_TEMPLATE_PART ],
     246                        array_column( wp_get_registered_entity_action_modules( 'postType', 'wp_template_part' ), 'actions', 'id' )[ self::WP_TEMPLATE_PART ],
    247247                        'The action comes from the `wp_template_part` module.'
    248248                );
    … …  
    523523                                self::PAGE               => self::PAGE_ACTIONS,
    524524                        ),
    525                         wp_get_registered_entity_action_modules( 'postType', 'page' ),
     525                        array_column( wp_get_registered_entity_action_modules( 'postType', 'page' ), 'actions', 'id' ),
    526526                        'The replaced action keeps the module of the core action.'
    527527                );
  • trunk/tests/phpunit/tests/entity-fields/wpAddEntityFieldModulesToEditorScript.php

    r64566 r64686  
    1313 *
    1414 * @covers ::_wp_add_entity_field_modules_to_editor_script
     15 * @covers ::_wp_add_entity_modules_to_editor_script
    1516 */
    1617class Tests_Entity_Fields_WpAddEntityFieldModulesToEditorScript extends WP_Entity_Fields_UnitTestCase {
  • trunk/tests/phpunit/tests/entity-fields/wpEntityFieldsRegistry.php

    r64528 r64686  
    130130                $this->assertSame( 'Author', $fields['author']['label'], 'The registered field is left untouched.' );
    131131                $this->assertSame( 'core', $fields['author']['origin']['registeredBy'] );
    132                 $this->assertSame( array( 'color' ), wp_get_registered_entity_field_modules( 'postType', 'page' )['plugin/fields'], 'The script module only applies to the fields registered.' );
     132                $this->assertSame( array( 'color' ), array_column( wp_get_registered_entity_field_modules( 'postType', 'page' ), 'fields', 'id' )['plugin/fields'], 'The script module only applies to the fields registered.' );
    133133                $this->assertCount( 1, $reported );
    134134                $this->assertStringContainsString( 'postType "page"', $reported[0], 'The notice names the entity.' );
    … …  
    218218                $this->assertSame( array(), wp_get_registered_entity_fields( 'a/b', 'c' ) );
    219219                $this->assertSame( array( 'color', 'size' ), array_column( wp_get_registered_entity_fields( 'a', 'b/c' ), 'id' ), 'The second entity keeps its fields.' );
    220                 $this->assertSame( array( 'plugin/second' => array( 'color', 'size' ) ), wp_get_registered_entity_field_modules( 'a', 'b/c' ), 'The second entity keeps only its own module.' );
     220                $this->assertSame(
     221                        array(
     222                                array(
     223                                        'id'     => 'plugin/second',
     224                                        'fields' => array( 'color', 'size' ),
     225                                ),
     226                        ),
     227                        wp_get_registered_entity_field_modules( 'a', 'b/c' ),
     228                        'The second entity keeps only its own module.'
     229                );
    221230                $this->assertSame( array( 'b/c' => array( 'plugin/second' ) ), wp_get_all_registered_entity_field_modules()['a'], 'Only the second entity has modules under its kind.' );
    222231                $this->assertArrayNotHasKey( 'a/b', wp_get_all_registered_entity_field_modules(), 'The kind of the first entity, emptied, is forgotten.' );
     232        }
     233
     234        /**
     235         * A numeric module id is listed as a string, as it was registered, not
     236         * as the integer its array key becomes.
     237         *
     238         * @ticket 66229
     239         */
     240        public function test_all_registered_field_modules_lists_a_numeric_module_id_as_a_string() {
     241                $this->register_fields( 'postType', 'post', array( $this->field( 'color' ) ), '42' );
     242
     243                $this->assertSame( array( '42' ), wp_get_all_registered_entity_field_modules()['postType']['post'] );
     244                $this->assertSame( array( '42' ), array_column( wp_get_registered_entity_field_modules( 'postType', 'post' ), 'id' ) );
    223245        }
    224246
    … …  
    323345                        $fields[0]['origin']
    324346                );
    325                 $modules = wp_get_registered_entity_field_modules( 'postType', 'page' );
     347                $modules = array_column( wp_get_registered_entity_field_modules( 'postType', 'page' ), 'fields', 'id' );
    326348                $this->assertContains( 'author', $modules['test/defaults'], 'The field keeps its modules.' );
    327349                $this->assertSame( array( 'author' ), $modules['plugin/author'], 'The module applies to the field.' );
    … …  
    397419                $this->assertSame( 'Writer', $fields['author']['label'], 'The rest of the fields of the call are updated.' );
    398420                $this->assertSame( array( 'test-plugin' ), $fields['author']['origin']['updatedBy'] );
    399                 $this->assertSame( array( 'author' ), wp_get_registered_entity_field_modules( 'postType', 'page' )['plugin/fields'], 'The script module only applies to the fields updated.' );
     421                $this->assertSame( array( 'author' ), array_column( wp_get_registered_entity_field_modules( 'postType', 'page' ), 'fields', 'id' )['plugin/fields'], 'The script module only applies to the fields updated.' );
    400422                $this->assertCount( 1, $reported );
    401423                $this->assertStringContainsString( 'postType "page"', $reported[0], 'The notice names the entity.' );
    … …  
    431453                                                ),
    432454                                                $this->field( 'weight' ),
     455                                                $this->field( '0' ),
    433456                                        )
    434457                                );
    … …  
    443466                                                        'label' => 'Colour',
    444467                                                ),
     468                                                array(
     469                                                        'id'    => '0',
     470                                                        'label' => 'Zero',
     471                                                ),
    445472                                        )
    446473                                );
    … …  
    448475                );
    449476
    450                 $this->assertSame( array( 'color', 'weight' ), $registered, 'The valid definitions are registered.' );
    451                 $this->assertSame( array( 'color' ), $updated, 'The valid definitions are updated.' );
     477                $this->assertSame( array( 'color', 'weight', '0' ), $registered, 'The valid definitions are registered, `0` included.' );
     478                $this->assertSame( array( 'color', '0' ), $updated, 'The valid definitions are updated, `0` included.' );
    452479                $fields = array_column( wp_get_registered_entity_fields( 'postType', 'page' ), null, 'id' );
    453480                $this->assertSame( 'Colour', $fields['color']['label'] );
     481                $this->assertSame( 'Zero', $fields['0']['label'] );
    454482                $this->assertArrayHasKey( 'weight', $fields );
    455483                $this->assertCount( 1, $reported );
    … …  
    558586                $this->assertSame( $fired, did_action( 'wp_entity_fields_api_init' ), 'Refusing does not fire the action.' );
    559587                $this->assertNotContains( 'color', array_column( wp_get_registered_entity_fields( 'postType', 'page' ), 'id' ) );
    560                 $this->assertArrayNotHasKey( 'plugin/color', wp_get_registered_entity_field_modules( 'postType', 'page' ) );
     588                $this->assertNotContains( 'plugin/color', array_column( wp_get_registered_entity_field_modules( 'postType', 'page' ), 'id' ) );
    561589        }
    562590
  • trunk/tests/phpunit/tests/entity-fields/wpPreloadEntityFields.php

    r64595 r64686  
    1212 * @group entity-fields
    1313 *
     14 * @covers ::_wp_add_entity_preload_paths
    1415 * @covers ::_wp_preload_entity_fields
    15  * @covers ::_wp_get_post_type_fields_preload_paths
    16  * @covers ::_wp_get_site_editor_screen_post_types
    1716 */
    1817class Tests_Entity_Fields_WpPreloadEntityFields extends WP_Entity_Fields_UnitTestCase {
    … …  
    141140         */
    142141        public function test_unregistered_post_types_are_not_preloaded() {
    143                 $this->assertSame(
    144                         array( '/wp/v2/entity-fields?kind=postType&name=page' ),
    145                         _wp_get_post_type_fields_preload_paths( array( 'page', 'not_a_post_type', 'page' ) )
    146                 );
     142                $context = new WP_Block_Editor_Context(
     143                        array(
     144                                'name' => 'core/edit-post',
     145                                'post' => new WP_Post( (object) array( 'post_type' => 'not_a_post_type' ) ),
     146                        )
     147                );
     148
     149                $this->assertSame( array(), _wp_preload_entity_fields( array(), $context ) );
    147150        }
    148151
    … …  
    162165                $this->assertSame( array(), _wp_preload_entity_fields( array(), $widgets ) );
    163166        }
     167
     168        /**
     169         * Empty paths that are not an array become an array the fields are added
     170         * to. Other paths that are not an array are returned unchanged.
     171         *
     172         * @ticket 66229
     173         *
     174         * @dataProvider data_non_array_paths
     175         *
     176         * @param mixed $paths    The paths passed to the filter.
     177         * @param mixed $expected The filtered paths, or null for the fields path
     178         *                        only.
     179         */
     180        public function test_non_array_paths( $paths, $expected ) {
     181                $post    = self::factory()->post->create_and_get( array( 'post_type' => 'page' ) );
     182                $context = new WP_Block_Editor_Context(
     183                        array(
     184                                'name' => 'core/edit-post',
     185                                'post' => $post,
     186                        )
     187                );
     188
     189                if ( null === $expected ) {
     190                        $expected = array( '/wp/v2/entity-fields?kind=postType&name=page' );
     191                }
     192
     193                $this->assertSame( $expected, _wp_preload_entity_fields( $paths, $context ) );
     194        }
     195
     196        /**
     197         * Data provider for test_non_array_paths().
     198         *
     199         * @return array[]
     200         */
     201        public function data_non_array_paths() {
     202                $object = new stdClass();
     203
     204                return array(
     205                        'null'         => array( null, null ),
     206                        'false'        => array( false, null ),
     207                        'empty string' => array( '', null ),
     208                        'string'       => array( '/wp/v2/types', '/wp/v2/types' ),
     209                        'object'       => array( $object, $object ),
     210                );
     211        }
     212
     213        /**
     214         * Called with the paths only, as by a filter added with one accepted
     215         * argument, the paths are returned unchanged.
     216         *
     217         * @ticket 66229
     218         */
     219        public function test_the_paths_are_unchanged_without_a_context() {
     220                $this->assertSame( array( '/wp/v2/types' ), _wp_preload_entity_fields( array( '/wp/v2/types' ) ) );
     221                $this->assertSame( array(), _wp_preload_entity_fields( null ) );
     222        }
    164223}
  • trunk/tests/phpunit/tests/entity-fields/wpRegisterCoreFieldCollections.php

    r64581 r64686  
    210210                $this->assertSame(
    211211                        array( self::POST_TYPE_SUPPORTS => array_column( $fields, 'id' ) ),
    212                         wp_get_registered_entity_field_modules( 'postType', 'test_book' ),
     212                        array_column( wp_get_registered_entity_field_modules( 'postType', 'test_book' ), 'fields', 'id' ),
    213213                        'Every default field is registered with the module of its folder.'
    214214                );
    … …  
    364364        public function test_pages_get_their_own_title_field() {
    365365                $fields  = self::get_fields( 'postType', 'page' );
    366                 $modules = wp_get_registered_entity_field_modules( 'postType', 'page' );
     366                $modules = array_column( wp_get_registered_entity_field_modules( 'postType', 'page' ), 'fields', 'id' );
    367367
    368368                $this->assertFalse( $fields['title']['enableHiding'], 'The page title is the one of the collection.' );
    … …  
    391391                $this->assertSame(
    392392                        $ids,
    393                         wp_get_registered_entity_field_modules( 'postType', 'wp_template' )['@wordpress/core-fields/wp_template'],
     393                        array_column( wp_get_registered_entity_field_modules( 'postType', 'wp_template' ), 'fields', 'id' )['@wordpress/core-fields/wp_template'],
    394394                        'The fields of templates ship their JavaScript parts in the module of the collection.'
    395395                );
    … …  
    413413                $this->assertSame(
    414414                        array( 'author', 'title' ),
    415                         wp_get_registered_entity_field_modules( 'postType', 'wp_template_part' )['@wordpress/core-fields/wp_template_part'],
     415                        array_column( wp_get_registered_entity_field_modules( 'postType', 'wp_template_part' ), 'fields', 'id' )['@wordpress/core-fields/wp_template_part'],
    416416                        'The fields of template parts ship their JavaScript parts in the module of the collection.'
    417417                );
    … …  
    426426        public function test_patterns_get_their_own_fields() {
    427427                $fields  = self::get_fields( 'postType', 'wp_block' );
    428                 $modules = wp_get_registered_entity_field_modules( 'postType', 'wp_block' );
     428                $modules = array_column( wp_get_registered_entity_field_modules( 'postType', 'wp_block' ), 'fields', 'id' );
    429429
    430430                $this->assertTrue( post_type_supports( 'wp_block', 'excerpt' ), 'The post type supports excerpts.' );
    … …  
    487487                $this->assertSame(
    488488                        array( '@wordpress/core-fields/attachment' => $ids ),
    489                         wp_get_registered_entity_field_modules( 'postType', 'attachment' ),
     489                        array_column( wp_get_registered_entity_field_modules( 'postType', 'attachment' ), 'fields', 'id' ),
    490490                        'Every media field is registered with the module of the collection.'
    491491                );
    … …  
    509509                $this->assertSame(
    510510                        array( '@wordpress/core-fields/root_site' => $ids ),
    511                         wp_get_registered_entity_field_modules( 'root', 'site' ),
     511                        array_column( wp_get_registered_entity_field_modules( 'root', 'site' ), 'fields', 'id' ),
    512512                        'Every field of the site is registered with the module of the collection.'
    513513                );
    … …  
    714714                );
    715715
    716                 $modules = wp_get_registered_entity_field_modules( 'postType', 'wp_template' );
     716                $modules = array_column( wp_get_registered_entity_field_modules( 'postType', 'wp_template' ), 'fields', 'id' );
    717717
    718718                $this->assertSame( 'my-plugin', self::get_fields( 'postType', 'wp_template' )['author']['origin']['registeredBy'], 'The author field is the one of the plugin.' );
  • trunk/tests/phpunit/tests/entity-fields/wpRegisterEntityFieldCollection.php

    r64528 r64686  
    101101                $this->assertSame( array( 'subtitle', 'isbn' ), array_column( $book, 'id' ), 'The fields registered first come first: the fixture collections run at priority 0.' );
    102102                $this->assertSame( 'fixture', $book[0]['origin']['registeredBy'], 'The fields carry the origin of their collection.' );
    103                 $this->assertSame( array( 'fixture/book' => array( 'subtitle' ) ), wp_get_registered_entity_field_modules( 'postType', 'test_book' ), 'Each field is registered with the module of its collection.' );
     103                $this->assertSame(
     104                        array(
     105                                array(
     106                                        'id'     => 'fixture/book',
     107                                        'fields' => array( 'subtitle' ),
     108                                ),
     109                        ),
     110                        wp_get_registered_entity_field_modules( 'postType', 'test_book' ),
     111                        'Each field is registered with the module of its collection.'
     112                );
    104113
    105114                $this->assertSame( array( 'issue' ), array_column( wp_get_registered_entity_fields( 'postType', 'test_magazine' ), 'id' ) );
  • trunk/tests/phpunit/tests/rest-api/rest-entity-fields-controller.php

    r64581 r64686  
    595595
    596596        /**
     597         * A numeric script module id is returned as a string, like any other.
     598         *
     599         * @covers ::get_items
     600         *
     601         * @ticket 66229
     602         */
     603        public function test_get_items_returns_a_numeric_module_id_as_a_string() {
     604                $this->register_fields(
     605                        'customKind',
     606                        'customName',
     607                        array(
     608                                array(
     609                                        'id'    => 'color',
     610                                        'type'  => 'text',
     611                                        'label' => 'Color',
     612                                ),
     613                        ),
     614                        '123'
     615                );
     616
     617                wp_set_current_user( self::$editor_id );
     618                $data = $this->dispatch_request( 'customKind', 'customName' )->get_data();
     619
     620                $this->assertSame(
     621                        array(
     622                                array(
     623                                        'id'     => '123',
     624                                        'fields' => array( 'color' ),
     625                                ),
     626                        ),
     627                        $data['script_modules']
     628                );
     629        }
     630
     631        /**
    597632         * Fields registered without a script module have none listed.
    598633         *
Note: See TracChangeset for help on using the changeset viewer.