Commit d1310ba782 for woocommerce
commit d1310ba78298dfded03a5fb4b7afbef62d14a49b
Author: Sam Seay <samueljseay@gmail.com>
Date: Fri Nov 28 03:14:02 2025 +1300
Enhance Hydration class to support urls with ID and query params (#62057)
diff --git a/plugins/woocommerce/changelog/62057-dev-enhance-hydration-class-for-url-params b/plugins/woocommerce/changelog/62057-dev-enhance-hydration-class-for-url-params
new file mode 100644
index 0000000000..dabb1b769e
--- /dev/null
+++ b/plugins/woocommerce/changelog/62057-dev-enhance-hydration-class-for-url-params
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add support to Hydration class to get store API responses with url and query params.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/Hydration.php b/plugins/woocommerce/src/Blocks/Domain/Services/Hydration.php
index 88513f16ee..e4b391e263 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Services/Hydration.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/Hydration.php
@@ -47,7 +47,7 @@ class Hydration {
// Allow-list only store API routes. No other request can be hydrated for safety.
$available_routes = StoreApi::container()->get( RoutesController::class )->get_all_routes( 'v1', true );
- $controller_class = $this->match_route_to_handler( $path, $available_routes );
+ $route_match = $this->match_route_to_handler( $path, $available_routes );
/**
* We disable nonce check to support endpoints such as checkout. The caveat here is that we need to be careful to only support GET requests. No other request type should be processed without nonce check. Additionally, no GET request can modify data as part of hydration request, for example adding items to cart.
@@ -60,9 +60,14 @@ class Hydration {
$preloaded_data = array();
- if ( null !== $controller_class ) {
+ if ( null !== $route_match ) {
try {
- $response = $this->get_response_from_controller( $controller_class, $path );
+ $response = $this->get_response_from_controller(
+ $route_match['controller'],
+ $path,
+ $route_match['url_params'],
+ $route_match['query_params']
+ );
if ( $response ) {
$preloaded_data = array(
'body' => $response->get_data(),
@@ -77,7 +82,7 @@ class Hydration {
'source' => 'blocks-hydration',
'data' => array(
'path' => $path,
- 'controller' => $controller_class,
+ 'controller' => $route_match['controller'] ?? null,
),
'backtrace' => true,
)
@@ -101,15 +106,28 @@ class Hydration {
*
* @param string $controller_class Controller class FQN that will respond to the request.
* @param string $path Request path regex.
+ * @param array $url_params URL parameters extracted from route (e.g., ['id' => '123']).
+ * @param array $query_params Query string parameters (e.g., ['key' => 'value']).
*
* @return false|mixed|null Response
*/
- private function get_response_from_controller( $controller_class, $path ) {
+ private function get_response_from_controller( $controller_class, $path, $url_params = array(), $query_params = array() ) {
if ( null === $controller_class ) {
return false;
}
- $request = new \WP_REST_Request( 'GET', $path );
+ $request = new \WP_REST_Request( 'GET', $path );
+
+ // Set URL parameters (from route segments like /products/123).
+ if ( ! empty( $url_params ) ) {
+ $request->set_url_params( $url_params );
+ }
+
+ // Set query parameters (from query string like ?key=value).
+ if ( ! empty( $query_params ) ) {
+ $request->set_query_params( $query_params );
+ }
+
$schema_controller = StoreApi::container()->get( SchemaController::class );
$controller = new $controller_class(
$schema_controller,
@@ -173,23 +191,41 @@ class Hydration {
/**
* Inspired from WP core's `match_request_to_handler`, this matches a given path from available route regexes.
- * However, unlike WP core, this does not check against query params, request method etc.
+ * Extracts URL parameters from regex named groups and query string parameters.
*
- * @param string $path The path to match.
+ * @param string $path The path to match (may include query string).
* @param array $available_routes Available routes in { $regex1 => $contoller_class1, ... } format.
*
- * @return string|null
+ * @return array|null Array with 'controller', 'url_params', and 'query_params' keys, or null if no match.
*/
private function match_route_to_handler( $path, $available_routes ) {
- $matched_route = null;
+ // Parse query string if present.
+ $query_params = array();
+ $parsed_url = wp_parse_url( $path );
+ $clean_path = $parsed_url['path'] ?? $path;
+
+ if ( isset( $parsed_url['query'] ) ) {
+ parse_str( $parsed_url['query'], $query_params );
+ }
+
+ // Match route and extract URL parameters.
foreach ( $available_routes as $route_path => $controller ) {
- $match = preg_match( '@^' . $route_path . '$@i', $path );
- if ( $match ) {
- $matched_route = $controller;
- break;
+ if ( preg_match( '@^' . $route_path . '$@i', $clean_path, $matches ) ) {
+ // Extract named groups (URL parameters like 'id').
+ $url_params = array_intersect_key(
+ $matches,
+ array_flip( array_filter( array_keys( $matches ), 'is_string' ) )
+ );
+
+ return array(
+ 'controller' => $controller,
+ 'url_params' => $url_params,
+ 'query_params' => $query_params,
+ );
}
}
- return $matched_route;
+
+ return null;
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/HydrationTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/HydrationTest.php
new file mode 100644
index 0000000000..21fdefb751
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/HydrationTest.php
@@ -0,0 +1,201 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\Domain\Services;
+
+use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
+use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
+use Automattic\WooCommerce\StoreApi\StoreApi;
+
+/**
+ * Tests for the Hydration class.
+ */
+class HydrationTest extends \WP_UnitTestCase {
+ /**
+ * Instance of Hydration for testing.
+ *
+ * @var Hydration
+ */
+ private $hydration;
+
+ /**
+ * Set up test fixtures.
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ // Initialize Store API.
+ StoreApi::container();
+
+ $this->hydration = new Hydration( $this->createMock( AssetDataRegistry::class ) );
+ }
+
+ /**
+ * Test that get_rest_api_response_data handles cart endpoint and returns valid cart structure.
+ */
+ public function test_get_rest_api_response_data_cart_without_params() {
+ $result = $this->hydration->get_rest_api_response_data( '/wc/store/v1/cart' );
+
+ $this->assertIsArray( $result );
+ $this->assertArrayHasKey( 'body', $result );
+ $this->assertArrayHasKey( 'headers', $result );
+
+ // Verify cart response has expected structure.
+ $cart = $result['body'];
+ $this->assertArrayHasKey( 'items', $cart, 'Cart should have items array' );
+ $this->assertArrayHasKey( 'totals', $cart, 'Cart should have totals' );
+ $this->assertArrayHasKey( 'coupons', $cart, 'Cart should have coupons array' );
+ $this->assertArrayHasKey( 'shipping_rates', $cart, 'Cart should have shipping_rates' );
+ $this->assertIsArray( $cart['items'] );
+ $this->assertIsArray( $cart['coupons'] );
+ $this->assertObjectHasProperty( 'total_items', $cart['totals'] );
+ $this->assertObjectHasProperty( 'total_price', $cart['totals'] );
+ $this->assertObjectHasProperty( 'currency_code', $cart['totals'] );
+ }
+
+ /**
+ * Test that get_rest_api_response_data handles invalid routes.
+ */
+ public function test_get_rest_api_response_data_handles_invalid_routes() {
+ $result = $this->hydration->get_rest_api_response_data( '/wc/store/v1/nonexistent' );
+
+ $this->assertIsArray( $result );
+ // Should fall back to rest_preload_api_request which may return empty or have body key.
+ $this->assertTrue(
+ empty( $result ) || isset( $result['body'] ),
+ 'Invalid route should return empty array or array with body key'
+ );
+ }
+
+ /**
+ * Test that get_rest_api_response_data handles products with stock_status query parameter.
+ */
+ public function test_get_rest_api_response_data_products_with_query_params() {
+ $out_of_stock_product = \WC_Helper_Product::create_simple_product();
+ $out_of_stock_product->set_name( 'Out of Stock Product' );
+ $out_of_stock_product->set_stock_status( 'outofstock' );
+ $out_of_stock_product->save();
+
+ $in_stock_product = \WC_Helper_Product::create_simple_product();
+ $in_stock_product->set_name( 'In Stock Product' );
+ $in_stock_product->set_stock_status( 'instock' );
+ $in_stock_product->save();
+
+ $result = $this->hydration->get_rest_api_response_data( '/wc/store/v1/products?stock_status[]=outofstock' );
+
+ $this->assertIsArray( $result );
+ $this->assertArrayHasKey( 'body', $result );
+ $this->assertIsArray( $result['body'] );
+ $this->assertNotEmpty( $result['body'], 'Should return at least one out of stock product' );
+
+ // Verify all returned products have outofstock status.
+ $found_out_of_stock = false;
+ foreach ( $result['body'] as $product_data ) {
+ $this->assertArrayHasKey( 'is_in_stock', $product_data );
+ $this->assertFalse( $product_data['is_in_stock'], 'Returned product should be out of stock' );
+
+ if ( $product_data['id'] === $out_of_stock_product->get_id() ) {
+ $found_out_of_stock = true;
+ }
+
+ // Ensure in stock product is NOT in results.
+ $this->assertNotEquals(
+ $in_stock_product->get_id(),
+ $product_data['id'],
+ 'In stock product should not appear in outofstock filter results'
+ );
+ }
+
+ $this->assertTrue( $found_out_of_stock, 'Out of stock product should be in results' );
+
+ $out_of_stock_product->delete( true );
+ $in_stock_product->delete( true );
+ }
+
+ /**
+ * Test that get_rest_api_response_data handles multiple query parameters (parent + type).
+ */
+ public function test_get_rest_api_response_data_with_multiple_query_params() {
+ $variable_product = \WC_Helper_Product::create_variation_product();
+ $variable_product->save();
+
+ $result = $this->hydration->get_rest_api_response_data(
+ '/wc/store/v1/products?parent[]=' . $variable_product->get_id() . '&type=variation'
+ );
+
+ $this->assertIsArray( $result );
+ $this->assertArrayHasKey( 'body', $result );
+ $this->assertIsArray( $result['body'] );
+
+ // Each returned product should be a variation of the parent.
+ foreach ( $result['body'] as $variation_data ) {
+ $this->assertArrayHasKey( 'id', $variation_data );
+ $this->assertArrayHasKey( 'parent', $variation_data );
+ $this->assertEquals( $variable_product->get_id(), $variation_data['parent'] );
+ $this->assertEquals( 'variation', $variation_data['type'] );
+ }
+
+ $variable_product->delete( true );
+ }
+
+ /**
+ * Test that get_rest_api_response_data handles product with ID in URL.
+ */
+ public function test_get_rest_api_response_data_product_with_id() {
+ $product = \WC_Helper_Product::create_simple_product();
+ $product->set_name( 'Test Product for Hydration' );
+ $product->save();
+
+ $result = $this->hydration->get_rest_api_response_data( '/wc/store/v1/products/' . $product->get_id() );
+
+ $this->assertIsArray( $result );
+ $this->assertArrayHasKey( 'body', $result );
+ $this->assertIsArray( $result['body'] );
+
+ if ( isset( $result['body']['id'] ) ) {
+ $this->assertEquals( $product->get_id(), $result['body']['id'] );
+ }
+ if ( isset( $result['body']['name'] ) ) {
+ $this->assertEquals( 'Test Product for Hydration', $result['body']['name'] );
+ }
+
+ $product->delete( true );
+ }
+
+ /**
+ * Test that encoded query parameters are properly handled.
+ */
+ public function test_get_rest_api_response_data_with_encoded_query_params() {
+ $matching_product = \WC_Helper_Product::create_simple_product();
+ $matching_product->set_name( 'Unique Hydration Test Product' );
+ $matching_product->save();
+
+ // Create a product that should not match in search results.
+ $non_matching_product = \WC_Helper_Product::create_simple_product();
+ $non_matching_product->set_name( 'Unrelated Item' );
+ $non_matching_product->save();
+
+ $result = $this->hydration->get_rest_api_response_data( '/wc/store/v1/products?search=Unique%20Hydration%20Test' );
+
+ $this->assertIsArray( $result );
+ $this->assertArrayHasKey( 'body', $result );
+ $this->assertIsArray( $result['body'] );
+
+ // Verify we got results and the matching product is included.
+ $this->assertNotEmpty( $result['body'], 'Search should return at least one product' );
+
+ $found_matching_product = false;
+ foreach ( $result['body'] as $product_data ) {
+ if ( $product_data['id'] === $matching_product->get_id() ) {
+ $found_matching_product = true;
+ $this->assertEquals( 'Unique Hydration Test Product', $product_data['name'] );
+ }
+ $this->assertNotEquals( $non_matching_product->get_id(), $product_data['id'], 'Non-matching product should not appear in search results' );
+ }
+
+ $this->assertTrue( $found_matching_product, 'Matching product should be in search results' );
+
+ $matching_product->delete( true );
+ $non_matching_product->delete( true );
+ }
+}