Last active
February 19, 2023 05:56
-
-
Save kadamwhite/0bd01441d70b400f62406c36e32ec22b to your computer and use it in GitHub Desktop.
Example of how to compose multiple API requests to efficiently fetch related resources.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* WP Post object. Only properties needed by the code are included. | |
* | |
* @typedef {object} WPPost | |
* @property {number} id ID of post. | |
* @property {number} author Post author ID. | |
* @property {number[]} categories IDs of associated categories. | |
* @property {number[]} tags IDs of associated tags. | |
* @property {number} featured_media ID of featured image. | |
*/ | |
/** | |
* Get a collection of REST resources. | |
* | |
* @param {string} route Route to GET. | |
* @param {object} [query] Query parameter map (optional). | |
* @param {boolean} [retry] Whether to allow retry on failure (optional). | |
* @returns {Promise} Promise to JSON results of query. | |
*/ | |
const get = async ( route, query = {}, retry = true ) => { | |
try { | |
const result = await fetch( `/wp-json${ route }?${ ( new URLSearchParams( query ) ).toString() }` ); | |
return result.json(); | |
} catch ( e ) { | |
if ( retry ) { | |
// Retry once. | |
return get( route, query, false ); | |
} | |
// Re-throw if failure. | |
throw e; | |
} | |
}; | |
/** | |
* Register and then fetch multiple API resources. | |
*/ | |
class Resource { | |
/** | |
* Construct the API Resource object. | |
* | |
* @param {string} route Collection endpoint for this API resource. | |
* @param {object} query Query parameters to use when fetching. | |
*/ | |
constructor( route, query = {} ) { | |
this.route = route; | |
this.query = query; | |
// Dictionary of resources to fetch, and later, their values. | |
this.resources = {}; | |
} | |
/** | |
* Prepare to fetch one or more resources by ID. | |
* | |
* @param {number|number[]} resourceId One or more resource IDs. | |
*/ | |
include( resourceId ) { | |
if ( Array.isArray( resourceId ) ) { | |
resourceId.forEach( ( id ) => { | |
this.resources[ id ] = true; | |
} ); | |
} else { | |
this.resources[ resourceId ] = true; | |
} | |
} | |
/** | |
* Set a resource value by ID. | |
* | |
* @param {number} id ID of resource. | |
* @param {object} resource Resource object. | |
*/ | |
set( id, resource ) { | |
this.resources[ id ] = resource; | |
} | |
/** | |
* Get one or more resources from the fetched data. | |
* | |
* @param {number|number[]} id ID of resource to return. | |
* @returns {object|number|number[]} Resource object, or unchanged ID if resource not found. | |
*/ | |
get( id ) { | |
return this.resources[ id ] || id; | |
} | |
/** | |
* Get multiple resources from the fetched data. | |
* | |
* @param {number[]} ids IDs of resource to return. | |
* @returns {Array} Array of resources, or their IDs if not found. | |
*/ | |
getMultiple( ids ) { | |
return ids.map( ( id ) => this.get( id ) ); | |
} | |
/** | |
* Fetch all registered IDs and store them in the resources dictionary. | |
* | |
* @async | |
* @returns {Promise<Array>} Resolves to array of returned resources. | |
*/ | |
async fetch() { | |
const ids = Object.keys( this.resources ); | |
const resources = await get( this.route, { | |
...this.query, | |
include: ids.join(), | |
per_page: ids.length, | |
} ); | |
resources.forEach( ( resource ) => { | |
this.set( resource.id, resource ); | |
} ); | |
return resources; | |
} | |
} | |
/** | |
* Get recent posts with minimal unnecessary fetching. | |
* | |
* @returns {Promise<object[]>} Promise to array of recent posts, including embedded values. | |
*/ | |
const getRecentPosts = async () => { | |
/** @type {WPPost[]} */ | |
let posts = []; | |
// Create instances of our Resource class for each "embedded" resource. | |
const authors = new Resource( '/wp/v2/users', { | |
_fields: 'id,link,name,avatar_urls', | |
} ); | |
const media = new Resource( '/wp/v2/media', { | |
_fields: 'id,media_details', | |
} ); | |
const tags = new Resource( '/wp/v2/tags', { | |
_fields: 'id,name,link', | |
} ); | |
const categories = new Resource( '/wp/v2/categories', { | |
_fields: 'id,name,link', | |
} ); | |
try { | |
// Fetch the posts. | |
posts = await get( '/wp/v2/posts', { | |
_fields: 'id,author,categories,date_gmt,excerpt,featured_media,link,modified_gmt,tags,title', | |
} ); | |
// Then set up the Resource objects with the IDs of linked resources. | |
posts.forEach( ( post ) => { | |
authors.include( post.author ); | |
media.include( post.featured_media ); | |
tags.include( post.tags ); | |
categories.include( post.categories ); | |
} ); | |
// Get all the "embedded" data in parallel. | |
await Promise.all( [ | |
authors.fetch(), | |
tags.fetch(), | |
categories.fetch(), | |
media.fetch(), | |
] ); | |
} catch ( e ) { | |
console.error( e ); | |
} | |
return posts.map( ( post ) => ( { | |
...post, | |
author: authors.get( post.author ), | |
tags: tags.getMultiple( post.tags ), | |
categories: categories.getMultiple( post.categories ), | |
media: media.get( post.featured_media ), | |
} ) ); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Fetch the posts, and log the results. | |
getRecentPosts().then( ( posts ) => { | |
console.log( posts ); | |
}, ( err ) => { | |
console.error( err ); | |
} ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Comment count is particularly difficult to find in bulk in the REST API. | |
* The best approach is for the theme or plugin to use register_rest_field | |
* to modify the Post response to include a custom value with the count of | |
* approved comments, if it is needed in your theme or plugin. | |
* | |
* An example of how to do this is provided here -- you would then also need | |
* to include `comment_count` in your `_fields=` query argument, above. | |
*/ | |
namespace My_Plugin; | |
/** | |
* Get the approved comment count for a post. | |
* | |
* @param \WP_Post $post Post for which to count comments. | |
* @return int Count of approved comments. | |
*/ | |
function get_comment_count( $post ) { | |
return (int) ( wp_count_comments( $post['id'] )->approved ?? 0 ); | |
} | |
/** | |
* Add a numeric `comment_count` field to Post objects in the REST API. | |
*/ | |
function register_comment_count_rest_field() { | |
register_rest_field( 'post', 'comment_count', [ | |
'get_callback' => __NAMESPACE__ . '\\get_comment_count', | |
'schema' => [ | |
'description' => __( 'Comment count for this post.', 'myplugin' ), | |
'type' => 'integer', | |
], | |
] ); | |
} | |
add_action( 'rest_api_init', __NAMESPACE__ . '\\register_comment_count_rest_field' ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment