Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,029 changes: 1,029 additions & 0 deletions src/wp-includes/class-wp-entity-fields-registry.php

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -856,4 +856,8 @@
// Entity Actions API.
add_action( 'admin_footer', '_wp_add_entity_action_modules_to_editor_script', 10, 0 );

// Entity Fields API.
add_action( 'admin_footer', '_wp_add_entity_field_modules_to_editor_script', 10, 0 );
add_filter( 'block_editor_rest_api_preload_paths', '_wp_preload_entity_fields', 10, 2 );

unset( $filter, $action, $post_type );
357 changes: 357 additions & 0 deletions src/wp-includes/entity-fields.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
<?php
/**
* Entity Fields API.
*
* A field definition is the serializable subset of the DataViews Field API:
* every property that is plain data (`id`, `type`, `label`, `elements`,
* `filterBy`, `isValid`, …). Properties that are JavaScript callbacks or
* components (`render`, `Edit` as a component, `getValue`, `setValue`, `sort`,
* `isVisible`, `getElements`, …) cannot be declared here. They are provided by
* a script module registered for the entity along with its fields: its default
* export maps field ids to the properties of each field that are JavaScript. An
* entity can have several modules. Each module applies to the fields it was
* registered with, see {@see wp_get_registered_entity_field_modules()}.
*
* Fields are registered on the `wp_entity_fields_api_init` action, and only there,
* with {@see wp_register_entity_fields()}, {@see wp_update_entity_fields()}, and
* {@see wp_unregister_entity_fields()}. The other functions read the registry.
*
* Fields can also be declared as files, grouped in a field collection, which
* a `wp_entity_fields_api_init` callback registers with
* {@see wp_register_entity_field_collection()}. A collection is for a single
* entity.
*
* The registered fields are exposed by the `/wp/v2/entity-fields` REST API
* route, see {@see WP_REST_Entity_Fields_Controller}.
*
* @package WordPress
* @subpackage Entity Fields API
* @since 7.2.0
*/

/**
* Registers fields for an entity, see
* {@see WP_Entity_Fields_Registry::register()}.
*
* It only runs on the `wp_entity_fields_api_init` action.
*
* @since 7.2.0
*
* @param string $origin Who registers the fields: `core`, or
* the slug of the plugin or theme.
* @param string $kind The entity kind (e.g. `postType`).
* @param string $name The entity name (e.g. `page`).
* @param array[] $fields The list of field definitions, each with an `id`.
* @param string|null $script_module The id of the script module providing
* the JavaScript parts of the fields, if any.
* @return string[] The ids of the fields registered, in the order of the
* call. Empty when none is.
*/
function wp_register_entity_fields( $origin, $kind, $name, $fields, $script_module = null ) {
return WP_Entity_Fields_Registry::get_instance()->register( $origin, $kind, $name, $fields, $script_module );
}

/**
* Updates registered fields of an entity, see
* {@see WP_Entity_Fields_Registry::update()}.
*
* It only runs on the `wp_entity_fields_api_init` action.
*
* @since 7.2.0
*
* @param string $origin Who updates the fields: `core`, or
* the slug of the plugin or theme.
* @param string $kind The entity kind (e.g. `postType`).
* @param string $name The entity name (e.g. `page`).
* @param array[] $fields The list of partial field definitions,
* each with the `id` of a registered field.
* @param string|null $script_module The id of the script module providing
* the JavaScript parts of the fields, if any.
* @return string[] The ids of the fields updated, in the order of the
* call. Empty when none is.
*/
function wp_update_entity_fields( $origin, $kind, $name, $fields, $script_module = null ) {
return WP_Entity_Fields_Registry::get_instance()->update( $origin, $kind, $name, $fields, $script_module );
}

/**
* Unregisters fields of an entity, see
* {@see WP_Entity_Fields_Registry::unregister()}.
*
* It only runs on the `wp_entity_fields_api_init` action.
*
* @since 7.2.0
*
* @param string $kind The entity kind (e.g. `postType`).
* @param string $name The entity name (e.g. `page`).
* @param string[]|null $ids The ids of the fields to unregister. Default
* null, every field of the entity.
* @return array[] The list of the definitions unregistered, in
* registration order. Empty when none is.
*/
function wp_unregister_entity_fields( $kind, $name, $ids = null ) {
return WP_Entity_Fields_Registry::get_instance()->unregister( $kind, $name, $ids );
}

/**
* Returns the fields registered for the given entity.
*
* @since 7.2.0
*
* @param string $kind The entity kind (e.g. `postType`).
* @param string $name The entity name (e.g. `page`).
* @return array[] The list of registered field definitions, in registration
* order. Empty until the `init` action has completed: a
* read before then is refused.
*/
function wp_get_registered_entity_fields( $kind, $name ) {
return WP_Entity_Fields_Registry::get_instance()->get_registered( $kind, $name );
}

/**
* Returns the ids of the script modules registered for the given entity.
*
* @since 7.2.0
*
* @param string $kind The entity kind (e.g. `postType`).
* @param string $name The entity name (e.g. `page`).
* @return array<string, string[]> The ids of the fields each module applies
* to, keyed by module id, in registration
* order. Empty until the `init` action has
* completed: a read before then is refused.
*/
function wp_get_registered_entity_field_modules( $kind, $name ) {
return WP_Entity_Fields_Registry::get_instance()->get_registered_field_modules( $kind, $name );
}

/**
* Returns the ids of all script modules registered for any entity.
*
* @since 7.2.0
*
* @return array<string, array<string, string[]>> The lists of module ids,
* keyed by kind, then by
* name. Empty until the
* `init` action has
* completed: a read before
* then is refused.
*/
function wp_get_all_registered_entity_field_modules() {
return WP_Entity_Fields_Registry::get_instance()->get_all_registered_field_modules();
}

/**
* Declares the script modules of the entity fields as dependencies of the
* editor script.
*
* The editor imports the script modules of an entity on demand, with a dynamic
* `import()` the browser resolves through the import map of the page. Listing
* the modules as dynamic `module_dependencies` of the `wp-editor` script adds
* them to the import map of every page that loads the editor script: the post
* editor, the site editor, and the pages booted by `@wordpress/boot`, whose
* prerequisites depend on it. A dynamic dependency is only listed in the
* import map; the module is fetched when it is imported.
*
* Reading the registry fires `wp_entity_fields_api_init`, so the fields and their
* script modules are registered on demand. It only reads the registry on the
* pages that load the editor script, not on every admin request (heartbeat,
* Ajax, other screens). It runs on `admin_footer`, the last action before the
* import map is printed that fires both in the admin template and in the pages
* rendered outside it, which print the import map themselves. By then `init`
* has completed, so the supports the default fields derive from are final.
* The registry only accepts registrations while the action fires, so the
* modules declared here are the modules of every field the page can show.
*
* It is hooked to `admin_footer` without arguments, so the global scripts
* registry is used. In other scenarios (tests), the scripts are given directly.
*
* @since 7.2.0
* @access private
*
* @param WP_Scripts|null $scripts Optional. The scripts registry. Default null,
* the global one.
*/
function _wp_add_entity_field_modules_to_editor_script( $scripts = null ) {
if ( ! $scripts instanceof WP_Scripts ) {
$scripts = wp_scripts();
}

/*
* The fields are loaded by the editor package, see
* packages/editor/src/dataviews/store/private-resolvers.ts in Gutenberg,
* which means any screen that wants to use this mechanism needs to load
* the editor script.
*/
if ( ! $scripts->query( 'wp-editor', 'enqueued' ) && ! $scripts->query( 'wp-editor', 'done' ) ) {
return;
}

$kinds = wp_get_all_registered_entity_field_modules();
if ( empty( $kinds ) ) {
return;
}

$dependencies = $scripts->get_data( 'wp-editor', 'module_dependencies' );
$dependencies = is_array( $dependencies ) ? $dependencies : array();

$declared = array();
foreach ( $dependencies as $dependency ) {
$declared[] = is_array( $dependency ) ? $dependency['id'] : $dependency;
}

$added = false;
foreach ( $kinds as $entities ) {
foreach ( $entities as $modules ) {
foreach ( $modules as $module ) {
if ( ! in_array( $module, $declared, true ) ) {
$dependencies[] = array(
'id' => $module,
'import' => 'dynamic',
);
$declared[] = $module;
$added = true;
}
}
}
}

if ( $added ) {
// `add_data()` replaces the value, hence the merge above.
$scripts->add_data( 'wp-editor', 'module_dependencies', $dependencies );
}
}

/**
* Returns the fields of a field collection, see
* {@see WP_Entity_Fields_Registry::get_collection_fields()}.
*
* @since 7.2.0
*
* @param string $directory The directory of the collection.
* @return array<string, array> The fields of the collection, keyed by id, in
* the alphabetical order of their folders.
*/
function wp_get_entity_field_collection_fields( $directory ) {
return WP_Entity_Fields_Registry::get_instance()->get_collection_fields( $directory );
}

/**
* Registers the fields of a field collection, see
* {@see WP_Entity_Fields_Registry::register_collection()} for the format of a
* collection.
*
* Like wp_register_entity_fields(), it only runs on the `wp_entity_fields_api_init`
* action.
*
* @since 7.2.0
*
* @param string $directory The directory of the collection.
* @return bool Whether the collection is valid and all of its fields are
* registered. True as well when it has no fields.
*/
function wp_register_entity_field_collection( $directory ) {
return WP_Entity_Fields_Registry::get_instance()->register_collection( $directory );
}

/**
* Returns the paths that serve the fields of the given post types, skipping
* the post types that are not registered.
*
* A path must match the request of the `getFieldsConfig` core data resolver,
* see packages/core-data/src/resolvers.js in Gutenberg.
*
* @since 7.2.0
* @access private
*
* @param string[] $post_types The post types.
* @return string[] One path of the `/wp/v2/entity-fields` route per post type,
* in the order the post types are given, without duplicates.
*/
function _wp_get_post_type_fields_preload_paths( $post_types ) {
$paths = array();
foreach ( array_unique( $post_types ) as $post_type ) {
if ( ! post_type_exists( $post_type ) ) {
continue;
}
$paths[] = add_query_arg(
array(
'kind' => 'postType',
'name' => $post_type,
),
'/wp/v2/entity-fields'
);
}

return $paths;
}

/**
* Returns the post types whose fields the screen of the site editor being
* loaded lists, from its `p` query arg.
*
* The screens are the routes in
* packages/edit-site/src/components/site-editor-routes in Gutenberg: the
* lists and the editors of pages, templates, patterns, and template parts.
* The patterns screen lists both patterns and template parts.
*
* @since 7.2.0
* @access private
*
* @return string[] The post types, empty for any other screen.
*/
function _wp_get_site_editor_screen_post_types() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only picks what to preload.
$path = isset( $_GET['p'] ) && is_string( $_GET['p'] ) ? sanitize_text_field( wp_unslash( $_GET['p'] ) ) : '';
$segments = explode( '/', trim( $path, '/' ) );

switch ( $segments[0] ) {
case 'page':
return array( 'page' );
case 'template':
case 'wp_template':
return array( 'wp_template' );
case 'pattern':
return array( 'wp_block', 'wp_template_part' );
case 'wp_block':
return array( 'wp_block' );
case 'wp_template_part':
return array( 'wp_template_part' );
}
return array();
}

/**
* Preloads the fields of the post types the site editor lists.
*
* The site editor reads them from the `/wp/v2/entity-fields` route before it
* can list the post fields, see `useFields` in
* packages/editor/src/dataviews/fields-loader/index.ts in Gutenberg: those of
* the edited post, if any, and of the post types of the screen it opens on.
*
* The post editor is left untouched: it only reads the fields in the
* DataForm-based inspector, an experiment of the Gutenberg plugin which
* preloads them on its own.
*
* @since 7.2.0
* @access private
*
* @param array $paths REST API paths to preload.
* @param WP_Block_Editor_Context $context Block editor context.
* @return array Filtered preload paths.
*/
function _wp_preload_entity_fields( $paths, $context ) {
if ( 'core/edit-site' !== $context->name ) {
return $paths;
}

$post_types = array();
if ( isset( $context->post ) ) {
$post_types[] = $context->post->post_type;
}
$post_types = array_merge( $post_types, _wp_get_site_editor_screen_post_types() );

return array_merge(
$paths,
_wp_get_post_type_fields_preload_paths( $post_types )
);
}
4 changes: 4 additions & 0 deletions src/wp-includes/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,10 @@ function create_initial_rest_routes() {
// Entity Actions.
$entity_actions_controller = new WP_REST_Entity_Actions_Controller();
$entity_actions_controller->register_routes();

// Entity Fields.
$entity_fields_controller = new WP_REST_Entity_Fields_Controller();
$entity_fields_controller->register_routes();
}

/**
Expand Down
Loading
Loading