Skip to content

Instantly share code, notes, and snippets.

@kadamwhite
Last active February 19, 2023 05:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kadamwhite/0bd01441d70b400f62406c36e32ec22b to your computer and use it in GitHub Desktop.
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.
/**
* 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 ),
} ) );
};
// Fetch the posts, and log the results.
getRecentPosts().then( ( posts ) => {
console.log( posts );
}, ( err ) => {
console.error( err );
} );
<?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