Commit 9a39207f2d for woocommerce
commit 9a39207f2d528268d8efc2a96e492823364aae63
Author: Néstor Soriano <konamiman@konamiman.com>
Date: Fri Dec 19 11:15:47 2025 +0100
Add caching and cache control headers for product REST API responses (#62258)
Co-authored-by: Nikhil <Nikschavan@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/pr-62258 b/plugins/woocommerce/changelog/pr-62258
new file mode 100644
index 0000000000..0ec7fe3e08
--- /dev/null
+++ b/plugins/woocommerce/changelog/pr-62258
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Add caching and cache control headers for product REST API responses
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
index 863dd97ee7..94bdcf7aee 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
@@ -44,12 +44,23 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
*/
protected $post_type = 'product_variation';
+ /**
+ * Get the default entity type for response caching.
+ *
+ * @return string|null The entity type.
+ */
+ protected function get_default_response_entity_type(): ?string {
+ return 'product_variation';
+ }
+
/**
* Register the routes for products.
*/
public function register_routes() {
register_rest_route(
- $this->namespace, '/' . $this->rest_base, array(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
@@ -58,7 +69,10 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
),
array(
'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_items' ),
+ 'callback' => $this->with_cache(
+ array( $this, 'get_items' ),
+ array( 'endpoint_id' => 'get_variations' )
+ ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
@@ -72,7 +86,9 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
)
);
register_rest_route(
- $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P<id>[\d]+)',
+ array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
@@ -85,7 +101,10 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
),
array(
'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_item' ),
+ 'callback' => $this->with_cache(
+ array( $this, 'get_item' ),
+ array( 'endpoint_id' => 'get_variation' )
+ ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param(
@@ -117,7 +136,9 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
)
);
register_rest_route(
- $this->namespace, '/' . $this->rest_base . '/batch', array(
+ $this->namespace,
+ '/' . $this->rest_base . '/batch',
+ array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
@@ -544,7 +565,9 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
if ( ! $object || 0 === $object->get_id() ) {
return new WP_Error(
- "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array(
+ "woocommerce_rest_{$this->post_type}_invalid_id",
+ __( 'Invalid ID.', 'woocommerce' ),
+ array(
'status' => 404,
)
);
@@ -652,7 +675,8 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
$injected_item = is_array( $item ) ? array_merge(
array(
'product_id' => $product_id,
- ), $item
+ ),
+ $item
) : $item;
if ( 'delete' === $batch_type && is_int( $item ) ) {
$injected_item = array(
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
index 669a28484f..4803c51428 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
@@ -13,6 +13,7 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
use Automattic\WooCommerce\Enums\ProductTaxStatus;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Enums\CatalogVisibility;
+use Automattic\WooCommerce\Internal\Traits\RestApiCache;
use Automattic\WooCommerce\Utilities\I18nUtil;
defined( 'ABSPATH' ) || exit;
@@ -25,6 +26,8 @@ defined( 'ABSPATH' ) || exit;
*/
class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
+ use RestApiCache;
+
/**
* Endpoint namespace.
*
@@ -58,6 +61,53 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
*/
public function __construct() {
add_action( "woocommerce_rest_insert_{$this->post_type}_object", array( $this, 'clear_transients' ) );
+ $this->initialize_rest_api_cache();
+ }
+
+ /**
+ * Get the default entity type for response caching.
+ *
+ * @return string|null The entity type.
+ */
+ protected function get_default_response_entity_type(): ?string {
+ return 'product';
+ }
+
+ /**
+ * Get data for ETag generation, excluding fields that change on each request.
+ *
+ * @param array $data Response data.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param string|null $endpoint_id Optional endpoint identifier.
+ * @return array Cleaned data for ETag generation.
+ */
+ protected function get_data_for_etag( array $data, WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
+ return $this->remove_related_ids_from_response_data( $data );
+ }
+
+ /**
+ * Remove related_ids from response data for ETag calculation.
+ *
+ * The related_ids field contains a random sample of related products,
+ * so it should not be used for ETag calculation.
+ *
+ * @param array $data Response data.
+ * @return array Response data without related_ids.
+ */
+ private function remove_related_ids_from_response_data( array $data ): array {
+ // Handle single product response.
+ if ( isset( $data['related_ids'] ) ) {
+ unset( $data['related_ids'] );
+ }
+
+ // Handle collection response (array of products).
+ foreach ( $data as $key => $item ) {
+ if ( is_array( $item ) && isset( $item['related_ids'] ) ) {
+ unset( $data[ $key ]['related_ids'] );
+ }
+ }
+
+ return $data;
}
/**
@@ -70,7 +120,10 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
array(
array(
'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_items' ),
+ 'callback' => $this->with_cache(
+ array( $this, 'get_items' ),
+ array( 'endpoint_id' => 'get_products' )
+ ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
@@ -96,7 +149,10 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
),
array(
'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_item' ),
+ 'callback' => $this->with_cache(
+ array( $this, 'get_item' ),
+ array( 'endpoint_id' => 'get_product' )
+ ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param(
@@ -141,6 +197,24 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
+
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P<id>[\d]+)/related',
+ array(
+ 'args' => array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
+ 'type' => 'integer',
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_related_products' ),
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
+ ),
+ )
+ );
}
/**
@@ -667,6 +741,26 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
return array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) );
}
+ /**
+ * Get related products for a specific product.
+ *
+ * @param WP_REST_Request<array<string, mixed>> $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ *
+ * @internal
+ */
+ public function get_related_products( $request ) {
+ $product = $this->get_object( (int) $request['id'] );
+
+ if ( ! $product instanceof \WC_Product || 0 === $product->get_id() ) {
+ return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
+ }
+
+ $related_ids = $this->api_get_related_ids( $product, 'view' );
+
+ return rest_ensure_response( array( 'related_ids' => $related_ids ) );
+ }
+
/**
* Fetch meta data.
*
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
index f69db66579..b497d4b582 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
@@ -93,7 +93,10 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
array(
array(
'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_suggested_products' ),
+ 'callback' => $this->with_cache(
+ array( $this, 'get_suggested_products' ),
+ array( 'endpoint_id' => 'get_suggested_products' )
+ ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_suggested_products_collection_params(),
),
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 70ccf95b30..bc4ddc74e8 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -77106,12 +77106,6 @@ parameters:
count: 1
path: src/Internal/RestApi/Routes/V4/Products/Controller.php
- -
- message: '#^Call to static method load\(\) on an unknown class Automattic\\WooCommerce\\Internal\\RestApi\\Routes\\V4\\Products\\WC_Data_Store\.$#'
- identifier: class.notFound
- count: 1
- path: src/Internal/RestApi/Routes/V4/Products/Controller.php
-
-
message: '#^Cannot access property \$public on WP_Post_Type\|null\.$#'
identifier: property.nonObject
@@ -79986,12 +79980,6 @@ parameters:
count: 1
path: src/Internal/Traits/AccessiblePrivateMethods.php
- -
- message: '#^Trait Automattic\\WooCommerce\\Internal\\Traits\\RestApiCache is used zero times and is not analysed\.$#'
- identifier: trait.unused
- count: 1
- path: src/Internal/Traits/RestApiCache.php
-
-
message: '#^Cannot call method format\(\) on DateTime\|false\.$#'
identifier: method.nonObject
@@ -84112,4 +84100,4 @@ parameters:
message: '#^Parameter \#1 \$string of function substr expects string, string\|false given\.$#'
identifier: argument.type
count: 1
- path: src/Utilities/StringUtil.php
+ path: src/Utilities/StringUtil.php
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
index bbb91c6131..edf7dcc7ce 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
@@ -105,7 +105,10 @@ class Controller extends WC_REST_Products_V2_Controller {
array(
array(
'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_suggested_products' ),
+ 'callback' => $this->with_cache(
+ array( $this, 'get_suggested_products' ),
+ array( 'endpoint_id' => 'get_suggested_products' )
+ ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_suggested_products_collection_params(),
),
@@ -2224,7 +2227,8 @@ class Controller extends WC_REST_Products_V2_Controller {
$exclude_ids = $request->get_param( 'exclude' );
$limit = $request->get_param( 'limit' ) ? $request->get_param( 'limit' ) : 5;
- $data_store = WC_Data_Store::load( 'product' );
+ $data_store = \WC_Data_Store::load( 'product' );
+ // @phpstan-ignore-next-line method.notFound
$this->suggested_products_ids = $data_store->get_related_products(
$categories,
$tags,
diff --git a/plugins/woocommerce/src/Internal/Traits/RestApiCache.php b/plugins/woocommerce/src/Internal/Traits/RestApiCache.php
index 0f60800b2f..3af2eb8d3c 100644
--- a/plugins/woocommerce/src/Internal/Traits/RestApiCache.php
+++ b/plugins/woocommerce/src/Internal/Traits/RestApiCache.php
@@ -203,12 +203,13 @@ trait RestApiCache {
* - If only cache headers are enabled: Execute the callback, generate ETag, and return 304
* if the client's ETag matches.
*
- * @param WP_REST_Request $request The request object.
- * @param callable $callback The original endpoint callback.
- * @param array $config Caching configuration specified for the endpoint.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param callable $callback The original endpoint callback.
+ * @param array $config Caching configuration specified for the endpoint.
+ *
* @return WP_REST_Response|\WP_Error The response.
*/
- private function handle_cacheable_request( WP_REST_Request $request, callable $callback, array $config ) {
+ private function handle_cacheable_request( WP_REST_Request $request, callable $callback, array $config ) { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
$backend_caching_enabled = ! is_null( $this->version_string_generator );
$cache_headers_enabled = 'yes' === get_option( 'woocommerce_rest_api_enable_cache_headers', 'yes' );
@@ -216,13 +217,14 @@ trait RestApiCache {
return call_user_func( $callback, $request );
}
+ $cached_config = null;
$should_skip_cache = ! $this->should_use_cache_for_request( $request );
if ( ! $should_skip_cache ) {
$cached_config = $this->build_cache_config( $request, $config );
$should_skip_cache = is_null( $cached_config );
}
- if ( $should_skip_cache ) {
+ if ( $should_skip_cache || is_null( $cached_config ) ) {
$response = call_user_func( $callback, $request );
if ( ! is_wp_error( $response ) ) {
$response = rest_ensure_response( $response );
@@ -252,10 +254,11 @@ trait RestApiCache {
/**
* Check if caching should be used for a particular incoming request.
*
- * @param WP_REST_Request $request The request object.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ *
* @return bool True if caching should be used, false otherwise.
*/
- private function should_use_cache_for_request( WP_REST_Request $request ): bool {
+ private function should_use_cache_for_request( WP_REST_Request $request ): bool { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
$skip_cache = $request->get_param( '_skip_cache' );
$should_cache = ! ( 'true' === $skip_cache || '1' === $skip_cache );
@@ -266,7 +269,7 @@ trait RestApiCache {
*
* @param bool $enable_caching Whether to enable response caching (result of !_skip_cache evaluation).
* @param object $controller The controller instance.
- * @param WP_REST_Request $request The request object.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
* @return bool True to enable response caching, false to disable.
*/
return apply_filters(
@@ -280,18 +283,21 @@ trait RestApiCache {
/**
* Build the output cache entry configuration from the request and per-endpoint config.
*
- * @param WP_REST_Request $request The request object.
- * @param array $config Raw configuration array passed to with_cache.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param array $config Raw configuration array passed to with_cache.
+ *
* @return array|null Normalized cache config with keys: endpoint_id, entity_type, vary_by_user, cache_ttl, relevant_hooks, include_headers, exclude_headers, cache_key. Returns null if entity type is not available.
+ *
* @throws \InvalidArgumentException If include_headers is not false or an array.
*/
- private function build_cache_config( WP_REST_Request $request, array $config ): ?array {
+ private function build_cache_config( WP_REST_Request $request, array $config ): ?array { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
$endpoint_id = $config['endpoint_id'] ?? null;
$entity_type = $config['entity_type'] ?? $this->get_default_response_entity_type();
$vary_by_user = $config['vary_by_user'] ?? $this->response_cache_vary_by_user( $request, $endpoint_id );
if ( ! $entity_type ) {
- wc_get_container()->get( LegacyProxy::class )->call_function(
+ $legacy_proxy = wc_get_container()->get( LegacyProxy::class );
+ $legacy_proxy->call_function(
'wc_doing_it_wrong',
__METHOD__,
'No entity type provided and no default entity type available. Skipping cache.',
@@ -328,13 +334,14 @@ trait RestApiCache {
* Supports both WP_REST_Response objects and raw data (which will be wrapped in a response object).
* Error objects are returned as-is without caching.
*
- * @param WP_REST_Request $request The request object.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
* @param WP_REST_Response|\WP_Error|array|object $response The response to potentially cache.
* @param array $cached_config Caching configuration from build_cache_config().
* @param bool $add_cache_headers Whether to add cache control headers.
+ *
* @return WP_REST_Response|\WP_Error The response with appropriate cache headers.
*/
- private function maybe_cache_response( WP_REST_Request $request, $response, array $cached_config, bool $add_cache_headers ) {
+ private function maybe_cache_response( WP_REST_Request $request, $response, array $cached_config, bool $add_cache_headers ) { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
if ( is_wp_error( $response ) ) {
return $response;
}
@@ -390,12 +397,13 @@ trait RestApiCache {
* if the client's If-None-Match header matches. It can be used both with and without
* backend caching.
*
- * @param WP_REST_Request $request The request object.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
* @param WP_REST_Response|\WP_Error|array|object $response The response to add headers to.
* @param array $cached_config Caching configuration from build_cache_config().
+ *
* @return WP_REST_Response|\WP_Error The response with cache headers.
*/
- private function maybe_add_cache_headers( WP_REST_Request $request, $response, array $cached_config ) {
+ private function maybe_add_cache_headers( WP_REST_Request $request, $response, array $cached_config ) { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
if ( is_wp_error( $response ) ) {
return $response;
}
@@ -413,11 +421,12 @@ trait RestApiCache {
$request_etag = $request->get_header( 'if-none-match' );
- $is_user_logged_in = wc_get_container()->get( LegacyProxy::class )->call_function( 'is_user_logged_in' );
+ $legacy_proxy = wc_get_container()->get( LegacyProxy::class );
+ $is_user_logged_in = $legacy_proxy->call_function( 'is_user_logged_in' );
$cache_visibility = $cached_config['vary_by_user'] && $is_user_logged_in ? 'private' : 'public';
$cache_control_value = $cache_visibility . ', must-revalidate, max-age=' . $cached_config['cache_ttl'];
- if ( ! empty( $response_etag ) && $request_etag === $response_etag ) {
+ if ( $request_etag === $response_etag ) {
$not_modified_response = $this->create_not_modified_response( $response_etag, $cache_control_value, $request, $cached_config['endpoint_id'] );
if ( $not_modified_response ) {
return $not_modified_response;
@@ -437,13 +446,14 @@ trait RestApiCache {
/**
* Create a 304 Not Modified response if allowed by filters.
*
- * @param string $etag The ETag value.
- * @param string $cache_control_value The Cache-Control header value.
- * @param WP_REST_Request $request The request object.
- * @param string|null $endpoint_id The endpoint identifier.
+ * @param string $etag The ETag value.
+ * @param string $cache_control_value The Cache-Control header value.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param string|null $endpoint_id The endpoint identifier.
+ *
* @return WP_REST_Response|null 304 response if allowed, null otherwise.
*/
- private function create_not_modified_response( string $etag, string $cache_control_value, WP_REST_Request $request, ?string $endpoint_id ): ?WP_REST_Response {
+ private function create_not_modified_response( string $etag, string $cache_control_value, WP_REST_Request $request, ?string $endpoint_id ): ?WP_REST_Response { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
$response = new WP_REST_Response( null, 304 );
$response->header( 'ETag', $etag );
$response->header( 'Cache-Control', $cache_control_value );
@@ -481,12 +491,13 @@ trait RestApiCache {
* Override in classes to exclude fields that change on each request
* (e.g., random recommendations, timestamps).
*
- * @param array $data Response data.
- * @param WP_REST_Request $request The request object.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param array $data Response data.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return array Cleaned data for ETag generation.
*/
- protected function get_data_for_etag( array $data, WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ protected function get_data_for_etag( array $data, WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Squiz.Commenting.FunctionComment.IncorrectTypeHint
return $data;
}
@@ -499,11 +510,12 @@ trait RestApiCache {
* This can be customized per-endpoint via the config array
* passed to with_cache() ('vary_by_user' key).
*
- * @param WP_REST_Request $request The request object.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return bool True to make cache user-specific, false otherwise.
*/
- protected function response_cache_vary_by_user( WP_REST_Request $request, ?string $endpoint_id = null ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ protected function response_cache_vary_by_user( WP_REST_Request $request, ?string $endpoint_id = null ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Squiz.Commenting.FunctionComment.IncorrectTypeHint
return true;
}
@@ -513,11 +525,12 @@ trait RestApiCache {
* This can be customized per-endpoint via the config array
* passed to with_cache() ('cache_ttl' key).
*
- * @param WP_REST_Request $request The request object.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return int Cache TTL in seconds.
*/
- protected function get_ttl_for_cached_response( WP_REST_Request $request, ?string $endpoint_id = null ): int { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ protected function get_ttl_for_cached_response( WP_REST_Request $request, ?string $endpoint_id = null ): int { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Squiz.Commenting.FunctionComment.IncorrectTypeHint
return HOUR_IN_SECONDS;
}
@@ -532,11 +545,12 @@ trait RestApiCache {
* This can be customized per-endpoint via the config array
* passed to with_cache() ('relevant_hooks' key).
*
- * @param WP_REST_Request $request Request object.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param WP_REST_Request<array<string, mixed>> $request Request object.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return array Array of hook names to track.
*/
- protected function get_hooks_relevant_to_caching( WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ protected function get_hooks_relevant_to_caching( WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Squiz.Commenting.FunctionComment.IncorrectTypeHint
return array();
}
@@ -551,11 +565,12 @@ trait RestApiCache {
* This can be customized per-endpoint via the config array
* passed to with_cache() ('include_headers' key).
*
- * @param WP_REST_Request $request Request object.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param WP_REST_Request<array<string, mixed>> $request Request object.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return array|false Array of header names to include (case-insensitive), or false to use exclusion logic.
*/
- protected function get_response_headers_to_include_in_caching( WP_REST_Request $request, ?string $endpoint_id = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ protected function get_response_headers_to_include_in_caching( WP_REST_Request $request, ?string $endpoint_id = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Squiz.Commenting.FunctionComment.IncorrectTypeHint
return false;
}
@@ -571,11 +586,12 @@ trait RestApiCache {
* This can be customized per-endpoint via the config array
* passed to with_cache() ('exclude_headers' key).
*
- * @param WP_REST_Request $request Request object.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param WP_REST_Request<array<string, mixed>> $request Request object.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return array Array of header names to exclude (case-insensitive).
*/
- protected function get_response_headers_to_exclude_from_caching( WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ protected function get_response_headers_to_exclude_from_caching( WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Squiz.Commenting.FunctionComment.IncorrectTypeHint
return array();
}
@@ -588,12 +604,13 @@ trait RestApiCache {
*
* Controllers can override this method to customize entity ID extraction.
*
- * @param array $response_data Response data.
- * @param WP_REST_Request $request The request object.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param array $response_data Response data.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return array Array of entity IDs.
*/
- protected function extract_entity_ids_from_response( array $response_data, WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ protected function extract_entity_ids_from_response( array $response_data, WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Squiz.Commenting.FunctionComment.IncorrectTypeHint
$ids = array();
if ( isset( $response_data[0] ) && is_array( $response_data[0] ) ) {
@@ -606,9 +623,10 @@ trait RestApiCache {
$ids[] = $response_data['id'];
}
- // Filter out null/false values but keep 0 and empty strings as they could be valid IDs.
+ // Filter out false values but keep 0 and empty strings as they could be valid IDs.
+ // Note: null values can't exist here because isset() checks above exclude them.
return array_unique(
- array_filter( $ids, fn ( $id ) => ! is_null( $id ) && false !== $id )
+ array_filter( $ids, fn ( $id ) => false !== $id )
);
}
@@ -626,15 +644,16 @@ trait RestApiCache {
* re-introducing dangerous headers like Set-Cookie.
* 5. Only headers from the response that are in the filtered list are returned.
*
- * @param array $nominal_headers Response headers.
- * @param array|false $include_headers Header names to include (false to use exclusion logic).
- * @param array $exclude_headers Header names to exclude (case-insensitive).
- * @param WP_REST_Request $request The request object.
- * @param WP_REST_Response $response The response object.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param array $nominal_headers Response headers.
+ * @param array|false $include_headers Header names to include (false to use exclusion logic).
+ * @param array $exclude_headers Header names to exclude (case-insensitive).
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param WP_REST_Response $response The response object.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return array Filtered headers array.
*/
- private function get_headers_to_cache( array $nominal_headers, $include_headers, array $exclude_headers, WP_REST_Request $request, WP_REST_Response $response, ?string $endpoint_id ): array {
+ private function get_headers_to_cache( array $nominal_headers, $include_headers, array $exclude_headers, WP_REST_Request $request, WP_REST_Response $response, ?string $endpoint_id ): array { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
// Step 1: Determine which headers to consider based on include/exclude.
if ( false !== $include_headers ) {
$include_headers_lowercase = array_map( 'strtolower', $include_headers );
@@ -675,6 +694,7 @@ trait RestApiCache {
* @param WP_REST_Response $response The response object.
* @param string|null $endpoint_id Optional friendly identifier for the endpoint.
* @param object $controller The controller instance.
+ *
* @return array Filtered list of header names to cache.
*/
$filtered_header_names = apply_filters(
@@ -695,7 +715,8 @@ trait RestApiCache {
);
if ( ! empty( $reintroduced_headers ) ) {
- wc_get_container()->get( LegacyProxy::class )->call_function(
+ $legacy_proxy = wc_get_container()->get( LegacyProxy::class );
+ $legacy_proxy->call_function(
'wc_doing_it_wrong',
__METHOD__,
sprintf(
@@ -723,12 +744,13 @@ trait RestApiCache {
/**
* Get cache key information that uniquely identifies a request.
*
- * @param WP_REST_Request $request The request object.
- * @param bool $vary_by_user Whether to include user ID in cache key.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param bool $vary_by_user Whether to include user ID in cache key.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return array Array of cache key information parts.
*/
- protected function get_key_info_for_cached_response( WP_REST_Request $request, bool $vary_by_user = false, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+ protected function get_key_info_for_cached_response( WP_REST_Request $request, bool $vary_by_user = false, ?string $endpoint_id = null ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Squiz.Commenting.FunctionComment.IncorrectTypeHint
$request_query_params = $request->get_query_params();
if ( is_array( $request_query_params ) ) {
ksort( $request_query_params );
@@ -741,7 +763,9 @@ trait RestApiCache {
);
if ( $vary_by_user ) {
- $user_id = wc_get_container()->get( LegacyProxy::class )->call_function( 'get_current_user_id' );
+ $legacy_proxy = wc_get_container()->get( LegacyProxy::class );
+ // @phpstan-ignore-next-line argument.type -- get_current_user_id returns int at runtime.
+ $user_id = intval( $legacy_proxy->call_function( 'get_current_user_id' ) );
$cache_key_parts[] = "user_{$user_id}";
}
@@ -751,13 +775,14 @@ trait RestApiCache {
/**
* Generate a cache key for a given request.
*
- * @param WP_REST_Request $request The request object.
- * @param string $entity_type The entity type.
- * @param bool $vary_by_user Whether to include user ID in cache key.
- * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param string $entity_type The entity type.
+ * @param bool $vary_by_user Whether to include user ID in cache key.
+ * @param string|null $endpoint_id Optional friendly identifier for the endpoint.
+ *
* @return string Cache key.
*/
- private function get_key_for_cached_response( WP_REST_Request $request, string $entity_type, bool $vary_by_user = false, ?string $endpoint_id = null ): string {
+ private function get_key_for_cached_response( WP_REST_Request $request, string $entity_type, bool $vary_by_user = false, ?string $endpoint_id = null ): string { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
$cache_key_parts = $this->get_key_info_for_cached_response( $request, $vary_by_user, $endpoint_id );
/**
@@ -768,10 +793,11 @@ trait RestApiCache {
* @since 10.5.0
*
* @param array $cache_key_parts Array of cache key information parts.
- * @param WP_REST_Request $request The request object.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
* @param bool $vary_by_user Whether user ID is included in cache key.
* @param string|null $endpoint_id Optional friendly identifier for the endpoint (passed to with_cache).
* @param object $controller The controller instance.
+ *
* @return array Filtered cache key information parts.
*/
$cache_key_parts = apply_filters(
@@ -791,6 +817,7 @@ trait RestApiCache {
* Generate a hash based on the actual usages of the hooks that affect the response.
*
* @param array $hook_names Array of hook names to track.
+ *
* @return string Hooks hash.
*/
private function generate_hooks_hash( array $hook_names ): string {
@@ -823,18 +850,20 @@ trait RestApiCache {
$this
);
- return md5( wp_json_encode( $cache_hash_data ) );
+ $json = wp_json_encode( $cache_hash_data );
+ return md5( false === $json ? '' : $json );
}
/**
* Get a cached response, but only if it's valid (otherwise the cached response will be invalidated).
*
- * @param WP_REST_Request $request The request object.
- * @param array $cached_config Built caching configuration from build_cache_config().
- * @param bool $cache_headers_enabled Whether to add cache control headers.
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param array $cached_config Built caching configuration from build_cache_config().
+ * @param bool $cache_headers_enabled Whether to add cache control headers.
+ *
* @return WP_REST_Response|null Cached response, or null if not available or has been invalidated.
*/
- private function get_cached_response( WP_REST_Request $request, array $cached_config, bool $cache_headers_enabled ): ?WP_REST_Response {
+ private function get_cached_response( WP_REST_Request $request, array $cached_config, bool $cache_headers_enabled ): ?WP_REST_Response { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
$cache_key = $cached_config['cache_key'];
$entity_type = $cached_config['entity_type'];
$cache_ttl = $cached_config['cache_ttl'];
@@ -843,11 +872,12 @@ trait RestApiCache {
$found = false;
$cached = wp_cache_get( $cache_key, self::$cache_group, false, $found );
- if ( ! $found || ! array_key_exists( 'data', $cached ) || ! isset( $cached['entity_versions'], $cached['created_at'] ) ) {
+ if ( ! $found || ! is_array( $cached ) || ! array_key_exists( 'data', $cached ) || ! isset( $cached['entity_versions'], $cached['created_at'] ) ) {
return null;
}
- $current_time = wc_get_container()->get( LegacyProxy::class )->call_function( 'time' );
+ $legacy_proxy = wc_get_container()->get( LegacyProxy::class );
+ $current_time = $legacy_proxy->call_function( 'time' );
$expiration_time = $cached['created_at'] + $cache_ttl;
if ( $current_time >= $expiration_time ) {
wp_cache_delete( $cache_key, self::$cache_group );
@@ -864,12 +894,14 @@ trait RestApiCache {
}
}
- foreach ( $cached['entity_versions'] as $entity_id => $cached_version ) {
- $version_id = "{$entity_type}_{$entity_id}";
- $current_version = $this->version_string_generator->get_version( $version_id );
- if ( $current_version !== $cached_version ) {
- wp_cache_delete( $cache_key, self::$cache_group );
- return null;
+ if ( ! is_null( $this->version_string_generator ) ) {
+ foreach ( $cached['entity_versions'] as $entity_id => $cached_version ) {
+ $version_id = "{$entity_type}_{$entity_id}";
+ $current_version = $this->version_string_generator->get_version( $version_id );
+ if ( $current_version !== $cached_version ) {
+ wp_cache_delete( $cache_key, self::$cache_group );
+ return null;
+ }
}
}
@@ -882,7 +914,8 @@ trait RestApiCache {
$response_headers = array();
if ( $cache_headers_enabled ) {
- $is_user_logged_in = wc_get_container()->get( LegacyProxy::class )->call_function( 'is_user_logged_in' );
+ $legacy_proxy = wc_get_container()->get( LegacyProxy::class );
+ $is_user_logged_in = $legacy_proxy->call_function( 'is_user_logged_in' );
$cache_visibility = $cached_config['vary_by_user'] && $is_user_logged_in ? 'private' : 'public';
if ( ! empty( $cached_etag ) ) {
@@ -892,12 +925,13 @@ trait RestApiCache {
// If the server adds a 'Date' header by itself there will be two such headers in the response.
// To help disambiguate them, we add also an 'X-WC-Date' header with the proper value.
- $created_at = gmdate( 'D, d M Y H:i:s', $cached['created_at'] ) . ' GMT';
+ // @phpstan-ignore-next-line argument.type -- created_at is int, stored by store_cached_response.
+ $created_at = gmdate( 'D, d M Y H:i:s', intval( $cached['created_at'] ) ) . ' GMT';
$response_headers['Date'] = $created_at;
$response_headers['X-WC-Date'] = $created_at;
if ( ! empty( $cached_etag ) && $request_etag === $cached_etag ) {
- $cache_control = $response_headers['Cache-Control'] ?? '';
+ $cache_control = $response_headers['Cache-Control'];
$not_modified_response = $this->create_not_modified_response( $cached_etag, $cache_control, $request, $cached_config['endpoint_id'] );
if ( $not_modified_response ) {
$not_modified_response->header( 'Date', $response_headers['Date'] );
@@ -937,18 +971,21 @@ trait RestApiCache {
*/
private function store_cached_response( string $cache_key, $data, int $status_code, string $entity_type, array $entity_ids, int $cache_ttl, array $relevant_hooks, array $headers = array(), string $etag = '' ): void {
$entity_versions = array();
- foreach ( $entity_ids as $entity_id ) {
- $version_id = "{$entity_type}_{$entity_id}";
- $version = $this->version_string_generator->get_version( $version_id );
- if ( $version ) {
- $entity_versions[ $entity_id ] = $version;
+ if ( ! is_null( $this->version_string_generator ) ) {
+ foreach ( $entity_ids as $entity_id ) {
+ $version_id = "{$entity_type}_{$entity_id}";
+ $version = $this->version_string_generator->get_version( $version_id );
+ if ( $version ) {
+ $entity_versions[ $entity_id ] = $version;
+ }
}
}
- $cache_data = array(
+ $legacy_proxy = wc_get_container()->get( LegacyProxy::class );
+ $cache_data = array(
'data' => $data,
'entity_versions' => $entity_versions,
- 'created_at' => wc_get_container()->get( LegacyProxy::class )->call_function( 'time' ),
+ 'created_at' => $legacy_proxy->call_function( 'time' ),
);
if ( 200 !== $status_code ) {
@@ -976,6 +1013,7 @@ trait RestApiCache {
* @internal
*
* @param bool $send_no_cache_headers Whether to send no-cache headers.
+ *
* @return bool False if we're handling caching for this request, original value otherwise.
*/
public function handle_rest_send_nocache_headers( bool $send_no_cache_headers ): bool {