Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REST API: new endpoint to fetch post counts by post status #7773

Draft
wants to merge 1 commit into
base: trunk
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ public function register_routes() {
'schema' => array( $this, 'get_public_item_schema' ),
)
);

register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/count',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_count' ),
'permission_callback' => array( $this, 'get_count_permissions_check' ),
),
'schema' => array( $this, 'get_count_schema' ),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does absolutely nothing in GET responses. It's only available in the 'help' context, which is used for OPTIONS requests.

@TimothyBJacobs do you know if it's advisable, or even possible, to have a custom schema for a route here?

)
);
}

/**
Expand Down Expand Up @@ -3296,4 +3309,86 @@ private function prepare_taxonomy_limit_schema( array $query_params ) {

return $query_params;
}

/**
* Retrieves post counts for the post type.
*
* @since 6.8.0
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_count() {
$counts = wp_count_posts( $this->post_type );
$data = array();

Comment on lines +3320 to +3323
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function get_count() {
$counts = wp_count_posts( $this->post_type );
$data = array();
public function get_count( $request ) {
$counts = wp_count_posts( $this->post_type );
$data = array();
$fields = $this->get_fields_for_response( $request );

if ( ! empty( $counts ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is empty, I think this should return a 404 error ( wp_error here ).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about that for the following reasons:

  • The wp_count_posts function always returns an stdClass object, even when no posts exist, so an empty count is still a valid count result.
  • A 404 status code only indicates that the resource is missing. The resource exists, it performs a count, but returns zero values.
  • The endpoint should maintain consistent response structure regardless of count values.

Unless I'm missing something in wp_count_posts, 200 OK, therefore, is the most appropriate response code for this case as it's still a structured response.

/*
* The fields comprise all non-internal post statuses,
* including any custom statuses that may be registered.
* 'trash' is an exception, so if it exists, it is added separately.
*/
$post_stati = get_post_stati( array( 'internal' => false ) );

if ( get_post_status_object( 'trash' ) ) {
$post_stati[] = 'trash';
}
// Include all public statuses in the response if there is a count.
foreach ( $post_stati as $status ) {
if ( isset( $counts->$status ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ( isset( $counts->$status ) ) {
if ( rest_is_field_included( $status, $fields ) ) && isset( $counts->$status ) ) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above suggestion had an extra parenthesis:

if ( rest_is_field_included( $status, $fields ) ) && isset( $counts->$status ) ) {

As I understand it, this will check against $this->get_item_schema() so the fields won't exist.

/counts requires a unique schema.

I'm finding the problem is that the schema callback when registering a route is never fired, unless it's an OPTIONS request (the context is 'help'). So I'm not sure the REST API is set up to allow a response to have two separate schemas.

I guess this is a good thing.

It's part of the reason why I chose to add a separate endpoint in the first place WordPress/gutenberg#66294

There is the option to add a counts field to the existing schema. 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is the option to add a counts field to the existing schema

Integrating it into the current item schema isn't appropriate as it's not an item, it's related to the collection

$data[ $status ] = (int) $counts->$status;
}
}
}
return rest_ensure_response( $data );
}

/**
* Checks if a given request has access to read post counts.
*
* @since 6.8.0
*
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_count_permissions_check() {
$post_type = get_post_type_object( $this->post_type );

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
return false;
}

if ( ! current_user_can( $post_type->cap->read ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to read post counts for this post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}

return true;
}

/**
* Retrieves the post counts schema, conforming to JSON Schema.
*
* @since 6.8.0
*
* @return array Item schema data.
*/
public function get_count_schema() {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'post-counts',
'type' => 'object',
/*
* Use a pattern matcher for post status keys.
* This allows for custom post statuses to be included,
* which can be registered after the schema is generated.
*/
'patternProperties' => array(
'^\w+$' => array(
'description' => __( 'The number of posts for a given status.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
'additionalProperties' => false,
);
Comment on lines +3374 to +3392
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'post-counts',
'type' => 'object',
/*
* Use a pattern matcher for post status keys.
* This allows for custom post statuses to be included,
* which can be registered after the schema is generated.
*/
'patternProperties' => array(
'^\w+$' => array(
'description' => __( 'The number of posts for a given status.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
'additionalProperties' => false,
);
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'post-counts',
'type' => 'object',
'properties' => [];
);
$post_stati = get_post_stati( array( 'internal' => false ) );
if ( get_post_status_object( 'trash' ) ) {
$post_stati[] = 'trash';
}
foreach( $post_stati as $post_status ) {
$schema['properties'] = array(
'description' => __( 'The number of posts for a given status.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
}
return $schema;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using get_post_stati was the original implementation WordPress/gutenberg@3895345 (#66294)

We can use it, but the caveat is that all custom post statuses must be registered at the highest priority, otherwise the endpoint will not return them.

A comment here is probably enough for now.

}
Comment on lines +3374 to +3393
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'post-counts',
'type' => 'object',
/*
* Use a pattern matcher for post status keys.
* This allows for custom post statuses to be included,
* which can be registered after the schema is generated.
*/
'patternProperties' => array(
'^\w+$' => array(
'description' => __( 'The number of posts for a given status.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
'additionalProperties' => false,
);
}
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'post-counts',
'type' => 'object',
/*
* Use a pattern matcher for post status keys.
* This allows for custom post statuses to be included,
* which can be registered after the schema is generated.
*/
'patternProperties' => array(
'^\w+$' => array(
'description' => __( 'The number of posts for a given status.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
'additionalProperties' => false,
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}

It is important to add the add fields and this caching.

}
1 change: 1 addition & 0 deletions tests/phpunit/tests/rest-api/rest-posts-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ public function test_register_routes() {
$this->assertCount( 2, $routes['/wp/v2/posts'] );
$this->assertArrayHasKey( '/wp/v2/posts/(?P<id>[\d]+)', $routes );
$this->assertCount( 3, $routes['/wp/v2/posts/(?P<id>[\d]+)'] );
$this->assertArrayHasKey( '/wp/v2/pages/count', $routes );
}

public function test_context_param() {
Expand Down
Loading