Commit 678191ad9cf for woocommerce
commit 678191ad9cfb4d8243da1859e7434693f6774354
Author: Sergey Mitroshin <sergeymitr@gmail.com>
Date: Thu Nov 5 19:49:41 2026 -0500
Move UCP Store API utilities into their own namespace (#70726)
Move UCP Store API utilities into their own namespace.
diff --git a/plugins/woocommerce/changelog/refactor-ucp-utilities-namespace b/plugins/woocommerce/changelog/refactor-ucp-utilities-namespace
new file mode 100644
index 00000000000..f224aad3a94
--- /dev/null
+++ b/plugins/woocommerce/changelog/refactor-ucp-utilities-namespace
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Move the UCP Store API utility classes into their own namespace and drop the redundant prefix; no merchant-facing change.
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index ed5fcd99d31..d65f707e473 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -680,6 +680,13 @@ final class WooCommerce {
/**
* Returns true if the request is a store REST API or UCP API request.
*
+ * The UCP namespace counts as one: like the Store API it returns data rather than rendered
+ * pages, so callers can skip the same work — block and template registration, site tracking.
+ *
+ * This is a rendering decision only. StoreApi\Authentication keeps its own, narrower check
+ * and does not treat UCP as Store API: its filters assume a browser client with a nonce,
+ * which a UCP agent never sends, so UCP routes must not inherit them.
+ *
* @return bool
*/
public function is_store_api_request() {
diff --git a/plugins/woocommerce/src/Internal/AgentReadiness/StoreConditions.php b/plugins/woocommerce/src/Internal/AgentReadiness/StoreConditions.php
index a5acb976260..2a293b1fa8f 100644
--- a/plugins/woocommerce/src/Internal/AgentReadiness/StoreConditions.php
+++ b/plugins/woocommerce/src/Internal/AgentReadiness/StoreConditions.php
@@ -4,7 +4,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\AgentReadiness;
use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonHelper;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
defined( 'ABSPATH' ) || exit;
@@ -64,7 +64,7 @@ class StoreConditions {
),
array(
'id' => 'ucp_on',
- 'ok' => UcpUtils::is_enabled(),
+ 'ok' => Utils::is_enabled(),
'blocks' => 'ucp',
'label' => __( 'Universal Commerce Protocol (UCP) is off', 'woocommerce' ),
'why' => __( 'Shopping agents that use UCP cannot read your catalog.', 'woocommerce' ),
diff --git a/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php b/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php
index 89d293d6642..f100be59b78 100644
--- a/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php
+++ b/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php
@@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\Internal\Ucp;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* Serves the UCP discovery profile at `/.well-known/ucp`.
@@ -38,7 +38,7 @@ final class UcpDiscovery implements RegisterHooksInterface {
public function register_feature_hooks(): void {
global $wp_rewrite;
- $active = UcpUtils::is_enabled() && $wp_rewrite->using_permalinks();
+ $active = Utils::is_enabled() && $wp_rewrite->using_permalinks();
if ( $active ) {
add_rewrite_rule( self::RULE, 'index.php?' . self::QUERY_VAR . '=1', 'top' );
@@ -71,7 +71,7 @@ final class UcpDiscovery implements RegisterHooksInterface {
* @param string $feature_id Feature being toggled.
*/
public function maybe_flush_rewrite_rules( string $feature_id ): void {
- if ( UcpUtils::FEATURE_ID === $feature_id ) {
+ if ( Utils::FEATURE_ID === $feature_id ) {
update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' );
}
}
@@ -111,15 +111,15 @@ final class UcpDiscovery implements RegisterHooksInterface {
public function get_profile(): array {
return array(
'ucp' => array(
- 'version' => UcpUtils::VERSION,
+ 'version' => Utils::VERSION,
'services' => array(
'dev.ucp.shopping' => array(
array(
- 'version' => UcpUtils::VERSION,
+ 'version' => Utils::VERSION,
'transport' => 'rest',
'endpoint' => rest_url( 'wc/ucp/v1' ),
- 'spec' => 'https://ucp.dev/' . UcpUtils::VERSION . '/specification/overview',
- 'schema' => 'https://ucp.dev/' . UcpUtils::VERSION . '/services/shopping/rest.openapi.json',
+ 'spec' => 'https://ucp.dev/' . Utils::VERSION . '/specification/overview',
+ 'schema' => 'https://ucp.dev/' . Utils::VERSION . '/services/shopping/rest.openapi.json',
),
),
),
@@ -127,7 +127,7 @@ final class UcpDiscovery implements RegisterHooksInterface {
static function ( array $declaration ): array {
return array( $declaration );
},
- UcpUtils::capability_declarations()
+ Utils::capability_declarations()
),
),
'links' => array(
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php
index 344710b91f3..6d3fed42d56 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php
@@ -7,8 +7,8 @@ use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractCartRoute as StoreApiCartR
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\SessionHandler;
use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpCartMapper;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\CartMapper;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* Shared behaviour of the UCP cart routes.
@@ -51,7 +51,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
* @return array
*/
protected function ucp_metadata( string $status ): array {
- return UcpUtils::response_metadata( UcpUtils::CAPABILITY_CART, $status );
+ return Utils::response_metadata( Utils::CAPABILITY_CART, $status );
}
/**
@@ -67,7 +67,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
* @return \WP_REST_Response
*/
public function get_response( \WP_REST_Request $request ) {
- $refusal = UcpUtils::refuse_non_json_payload( $request, UcpUtils::CAPABILITY_CART );
+ $refusal = Utils::refuse_non_json_payload( $request, Utils::CAPABILITY_CART );
if ( null !== $refusal ) {
return $this->add_response_headers( $refusal );
}
@@ -297,7 +297,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
return new \WP_REST_Response(
array_merge(
array( 'ucp' => $this->ucp_metadata( 'success' ) ),
- ( new UcpCartMapper() )->build_cart( $this->cart_item_response(), $cart_id, $context )
+ ( new CartMapper() )->build_cart( $this->cart_item_response(), $cart_id, $context )
),
200
);
@@ -328,7 +328,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
* @return \WP_REST_Response
*/
protected function ucp_error( string $code, string $message, string $severity, int $status, string $path = '$' ): \WP_REST_Response {
- return UcpUtils::error_response( UcpUtils::CAPABILITY_CART, $code, $message, $severity, $status, $path );
+ return Utils::error_response( Utils::CAPABILITY_CART, $code, $message, $severity, $status, $path );
}
/**
@@ -349,7 +349,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
$message = (string) $error->get_error_message();
return $this->ucp_error(
- UcpUtils::cart_error_code( (string) $error->get_error_code(), 'TRANSPORT_ERROR' ),
+ Utils::cart_error_code( (string) $error->get_error_code(), 'TRANSPORT_ERROR' ),
'' !== $message ? $message : __( 'The cart could not be loaded. Please try again.', 'woocommerce' ),
$status >= 500 ? 'unrecoverable' : 'recoverable',
$status
@@ -365,7 +365,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
* @return \WP_REST_Response
*/
protected function ucp_not_found(): \WP_REST_Response {
- return UcpUtils::error_response( UcpUtils::CAPABILITY_CART, 'not_found', __( 'Cart not found.', 'woocommerce' ), 'unrecoverable', 200, '$.id' );
+ return Utils::error_response( Utils::CAPABILITY_CART, 'not_found', __( 'Cart not found.', 'woocommerce' ), 'unrecoverable', 200, '$.id' );
}
/**
@@ -503,7 +503,7 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
}
return $this->ucp_error(
- UcpUtils::cart_error_code( $error->getErrorCode() ),
+ Utils::cart_error_code( $error->getErrorCode() ),
$error->getMessage(),
'recoverable',
200,
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCatalogRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCatalogRoute.php
index f4313823ae2..ed6a3b4b394 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCatalogRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCatalogRoute.php
@@ -6,7 +6,7 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;
use Automattic\WooCommerce\Internal\ProductFeed\Mapping\UcpProductMapper;
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
use Automattic\WooCommerce\StoreApi\Schemas\V1\Ucp\UcpProductSchema;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* Shared behaviour of the UCP catalog routes.
@@ -27,7 +27,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
private $mapper;
/**
- * The UCP capability this route serves, one of the UcpUtils::CAPABILITY_* constants.
+ * The UCP capability this route serves, one of the Utils::CAPABILITY_* constants.
*
* @return string
*/
@@ -40,7 +40,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
* @return array
*/
protected function ucp_metadata( string $status ): array {
- return UcpUtils::response_metadata( $this->get_capability(), $status );
+ return Utils::response_metadata( $this->get_capability(), $status );
}
/**
@@ -70,7 +70,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
* @return \WP_REST_Response
*/
public function get_response( \WP_REST_Request $request ) {
- return UcpUtils::refuse_non_json_payload( $request, $this->get_capability() ) ?? parent::get_response( $request );
+ return Utils::refuse_non_json_payload( $request, $this->get_capability() ) ?? parent::get_response( $request );
}
/**
@@ -125,7 +125,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
* @return array
*/
protected function get_context( \WP_REST_Request $request ): array {
- $raw = UcpUtils::json_param( $request, 'context' );
+ $raw = Utils::json_param( $request, 'context' );
if ( ! is_array( $raw ) ) {
return array();
@@ -152,7 +152,7 @@ abstract class AbstractCatalogRoute extends AbstractRoute {
* @return \WP_REST_Response
*/
protected function ucp_error( string $code, string $message, string $severity, int $status ): \WP_REST_Response {
- return UcpUtils::error_response( $this->get_capability(), $code, $message, $severity, $status );
+ return Utils::error_response( $this->get_capability(), $code, $message, $severity, $status );
}
/**
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/Carts.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/Carts.php
index 7ef51f3d15e..fee2b5d5f8d 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/Carts.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/Carts.php
@@ -3,7 +3,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* UCP `POST /carts` route.
@@ -69,7 +69,7 @@ class Carts extends AbstractCartRoute {
* @return \WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
- $line_items = UcpUtils::json_param( $request, 'line_items' );
+ $line_items = Utils::json_param( $request, 'line_items' );
if ( null !== $line_items ) {
$desired = $this->parse_line_items( $line_items );
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CartsById.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CartsById.php
index 31eade8ffa1..a1d26f90bbe 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CartsById.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CartsById.php
@@ -3,7 +3,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* UCP `GET /carts/{id}` and `PUT /carts/{id}` route.
@@ -88,7 +88,7 @@ class CartsById extends AbstractCartRoute {
* @return \WP_REST_Response
*/
protected function get_route_update_response( \WP_REST_Request $request ) {
- $line_items = UcpUtils::json_param( $request, 'line_items' );
+ $line_items = Utils::json_param( $request, 'line_items' );
// `line_items` is the whole desired state, so omitting it is not the same as
// sending an empty list: it leaves the cart alone.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogLookup.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogLookup.php
index 5bf69f88559..f06cbf58cde 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogLookup.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogLookup.php
@@ -3,7 +3,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* UCP `POST /catalog/lookup` route.
@@ -50,7 +50,7 @@ class CatalogLookup extends AbstractCatalogRoute {
* @return string
*/
protected function get_capability(): string {
- return UcpUtils::CAPABILITY_CATALOG_LOOKUP;
+ return Utils::CAPABILITY_CATALOG_LOOKUP;
}
/**
@@ -84,7 +84,7 @@ class CatalogLookup extends AbstractCatalogRoute {
* @return \WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
- $raw_ids = UcpUtils::json_param( $request, 'ids' );
+ $raw_ids = Utils::json_param( $request, 'ids' );
if ( ! is_array( $raw_ids ) || empty( $raw_ids ) ) {
return $this->ucp_error( 'VALIDATION_ERROR', __( 'ids is required.', 'woocommerce' ), 'recoverable', 400 );
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogProduct.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogProduct.php
index dddeb507e41..5045627159a 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogProduct.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogProduct.php
@@ -3,7 +3,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* UCP `POST /catalog/product` route.
@@ -47,7 +47,7 @@ class CatalogProduct extends AbstractCatalogRoute {
* @return string
*/
protected function get_capability(): string {
- return UcpUtils::CAPABILITY_CATALOG_LOOKUP;
+ return Utils::CAPABILITY_CATALOG_LOOKUP;
}
/**
@@ -84,13 +84,13 @@ class CatalogProduct extends AbstractCatalogRoute {
* @return \WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
- $id = trim( strval( UcpUtils::json_param( $request, 'id' ) ) );
+ $id = trim( strval( Utils::json_param( $request, 'id' ) ) );
if ( '' === $id ) {
return $this->ucp_error( 'VALIDATION_ERROR', __( 'id is required.', 'woocommerce' ), 'recoverable', 400 );
}
- $selected = (array) ( UcpUtils::json_param( $request, 'selected' ) ?? array() );
+ $selected = (array) ( Utils::json_param( $request, 'selected' ) ?? array() );
// Checked before the product is loaded: an over-long selection is refused
// without paying for the lookup it would have been applied to.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogSearch.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogSearch.php
index 22764fd3274..c747164550c 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogSearch.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/CatalogSearch.php
@@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;
use Automattic\WooCommerce\Enums\CatalogVisibility;
use Automattic\WooCommerce\StoreApi\Utilities\ProductQuery;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* UCP `POST /catalog/search` route.
@@ -57,7 +57,7 @@ class CatalogSearch extends AbstractCatalogRoute {
* @return string
*/
protected function get_capability(): string {
- return UcpUtils::CAPABILITY_CATALOG_SEARCH;
+ return Utils::CAPABILITY_CATALOG_SEARCH;
}
/**
@@ -99,9 +99,9 @@ class CatalogSearch extends AbstractCatalogRoute {
* @return \WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
- $query = trim( (string) UcpUtils::json_param( $request, 'query' ) );
- $filters = (array) ( UcpUtils::json_param( $request, 'filters' ) ?? array() );
- $pagination = (array) ( UcpUtils::json_param( $request, 'pagination' ) ?? array() );
+ $query = trim( (string) Utils::json_param( $request, 'query' ) );
+ $filters = (array) ( Utils::json_param( $request, 'filters' ) ?? array() );
+ $pagination = (array) ( Utils::json_param( $request, 'pagination' ) ?? array() );
$context = $this->get_context( $request );
$invalid = $this->validate_search_input( $query, $filters );
diff --git a/plugins/woocommerce/src/StoreApi/RoutesController.php b/plugins/woocommerce/src/StoreApi/RoutesController.php
index 8a57702430f..c854eb4d06a 100644
--- a/plugins/woocommerce/src/StoreApi/RoutesController.php
+++ b/plugins/woocommerce/src/StoreApi/RoutesController.php
@@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\StoreApi;
use Automattic\WooCommerce\Internal\ShopperLists\ShopperListsController;
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* RoutesController class.
@@ -84,7 +84,7 @@ class RoutesController {
Routes\V1\ShopperListItemsByKey::IDENTIFIER => Routes\V1\ShopperListItemsByKey::class,
],
'ucp' => [
- // Universal Commerce Protocol endpoints, gated by UcpUtils::is_enabled().
+ // Universal Commerce Protocol endpoints, gated by Utils::is_enabled().
Routes\V1\Ucp\CatalogSearch::IDENTIFIER => Routes\V1\Ucp\CatalogSearch::class,
Routes\V1\Ucp\CatalogLookup::IDENTIFIER => Routes\V1\Ucp\CatalogLookup::class,
Routes\V1\Ucp\CatalogProduct::IDENTIFIER => Routes\V1\Ucp\CatalogProduct::class,
@@ -107,7 +107,7 @@ class RoutesController {
$this->register_routes( 'shopper_lists', self::$api_namespace . '/v1' );
}
- if ( UcpUtils::is_enabled() ) {
+ if ( Utils::is_enabled() ) {
$this->register_routes( 'ucp', 'wc/ucp/v1' );
}
}
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/UcpCartMapper.php b/plugins/woocommerce/src/StoreApi/Utilities/Ucp/CartMapper.php
similarity index 96%
rename from plugins/woocommerce/src/StoreApi/Utilities/UcpCartMapper.php
rename to plugins/woocommerce/src/StoreApi/Utilities/Ucp/CartMapper.php
index 8e03356aa8d..ae7a5e03522 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/UcpCartMapper.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/Ucp/CartMapper.php
@@ -1,7 +1,9 @@
<?php
declare( strict_types = 1 );
-namespace Automattic\WooCommerce\StoreApi\Utilities;
+namespace Automattic\WooCommerce\StoreApi\Utilities\Ucp;
+
+use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;
/**
* Maps a Store API cart response onto the UCP cart shape.
@@ -11,7 +13,7 @@ namespace Automattic\WooCommerce\StoreApi\Utilities;
* storefront. Stateless; the catalog's {@see \Automattic\WooCommerce\Internal\ProductFeed\Mapping\UcpProductMapper}
* is the same idea for products.
*/
-class UcpCartMapper {
+class CartMapper {
/**
* Build the UCP cart payload.
*
@@ -168,7 +170,7 @@ class UcpCartMapper {
foreach ( $errors as $error ) {
$messages[] = array(
'type' => 'warning',
- 'code' => UcpUtils::cart_error_code( (string) ( $error['code'] ?? '' ) ),
+ 'code' => Utils::cart_error_code( (string) ( $error['code'] ?? '' ) ),
'content' => (string) ( $error['message'] ?? '' ),
'severity' => 'recoverable',
'path' => '$.line_items',
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/Ucp/Utils.php b/plugins/woocommerce/src/StoreApi/Utilities/Ucp/Utils.php
new file mode 100644
index 00000000000..72418f31c89
--- /dev/null
+++ b/plugins/woocommerce/src/StoreApi/Utilities/Ucp/Utils.php
@@ -0,0 +1,206 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\StoreApi\Utilities\Ucp;
+
+use Automattic\WooCommerce\Utilities\FeaturesUtil;
+
+/**
+ * Shared helpers for the UCP (Universal Commerce Protocol) routes.
+ */
+class Utils {
+ /**
+ * UCP specification version these routes implement.
+ */
+ const VERSION = '2026-04-08';
+
+ /**
+ * Feature id gating the UCP routes, stored as `woocommerce_feature_ucp_enabled`.
+ */
+ const FEATURE_ID = 'ucp';
+
+ /**
+ * Capability served by `POST /catalog/search`.
+ */
+ const CAPABILITY_CATALOG_SEARCH = 'dev.ucp.shopping.catalog.search';
+
+ /**
+ * Capability served by `POST /catalog/lookup` and `POST /catalog/product`.
+ */
+ const CAPABILITY_CATALOG_LOOKUP = 'dev.ucp.shopping.catalog.lookup';
+
+ /**
+ * Capability served by the `/carts` routes.
+ */
+ const CAPABILITY_CART = 'dev.ucp.shopping.cart';
+
+ /**
+ * Whether the UCP routes are registered.
+ *
+ * @return bool
+ */
+ public static function is_enabled(): bool {
+ return FeaturesUtil::feature_is_enabled( self::FEATURE_ID );
+ }
+
+ /**
+ * Build the `ucp` envelope every UCP response carries.
+ *
+ * UCP requires `capabilities` to name only the capability relevant to the
+ * operation being answered, so each response declares exactly one.
+ *
+ * @param string $capability One of the CAPABILITY_* constants.
+ * @param string $status Response status, `success` or `error`.
+ * @return array
+ */
+ public static function response_metadata( string $capability, string $status ): array {
+ return array(
+ 'version' => self::VERSION,
+ 'status' => $status,
+ 'capabilities' => array(
+ $capability => array( self::capability_declaration( $capability ) ),
+ ),
+ );
+ }
+
+ /**
+ * Spec and schema references for every capability these routes serve, keyed by capability name.
+ *
+ * @return array
+ */
+ public static function capability_declarations(): array {
+ return array(
+ self::CAPABILITY_CATALOG_SEARCH => array(
+ 'version' => self::VERSION,
+ 'spec' => 'https://ucp.dev/' . self::VERSION . '/specification/catalog/search',
+ 'schema' => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/catalog_search.json',
+ ),
+ self::CAPABILITY_CATALOG_LOOKUP => array(
+ 'version' => self::VERSION,
+ 'spec' => 'https://ucp.dev/' . self::VERSION . '/specification/catalog/lookup',
+ 'schema' => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/catalog_lookup.json',
+ ),
+ self::CAPABILITY_CART => array(
+ 'version' => self::VERSION,
+ 'spec' => 'https://ucp.dev/' . self::VERSION . '/specification/cart',
+ 'schema' => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/cart.json',
+ ),
+ );
+ }
+
+ /**
+ * Spec and schema references for a capability.
+ *
+ * @param string $capability One of the CAPABILITY_* constants.
+ * @return array
+ */
+ private static function capability_declaration( string $capability ): array {
+ return self::capability_declarations()[ $capability ] ?? array( 'version' => self::VERSION );
+ }
+
+ /**
+ * Build a UCP error response.
+ *
+ * The envelope shape every route shares. `$status` follows the UCP split: a malformed
+ * request keeps a non-2xx status, a valid request that ends in a refusal is a business
+ * outcome reported at 200 through `ucp.status` and `messages`.
+ *
+ * @param string $capability One of the CAPABILITY_* constants.
+ * @param string $code Error code.
+ * @param string $message Error message.
+ * @param string $severity Error severity.
+ * @param int $status HTTP status.
+ * @param string $path JSONPath of the offending field.
+ * @return \WP_REST_Response
+ */
+ public static function error_response( string $capability, string $code, string $message, string $severity, int $status, string $path = '$' ): \WP_REST_Response {
+ return new \WP_REST_Response(
+ array(
+ 'ucp' => self::response_metadata( $capability, 'error' ),
+ 'messages' => array(
+ array(
+ 'type' => 'error',
+ 'code' => $code,
+ 'content' => $message,
+ 'severity' => $severity,
+ 'path' => $path,
+ 'content_type' => 'plain',
+ ),
+ ),
+ ),
+ $status
+ );
+ }
+
+ /**
+ * Refuse a request that brought a body or form fields under anything other than a JSON content type.
+ *
+ * A UCP request is a JSON document, so a form or query payload is never read in its
+ * place. A bare request, such as a `cancel`, carries nothing and is not refused.
+ *
+ * @param \WP_REST_Request $request Request object.
+ * @param string $capability One of the CAPABILITY_* constants.
+ *
+ * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
+ *
+ * @return \WP_REST_Response|null The 415 refusal, or null when the request may be read.
+ */
+ public static function refuse_non_json_payload( \WP_REST_Request $request, string $capability ): ?\WP_REST_Response {
+ if ( $request->is_json_content_type() ) {
+ return null;
+ }
+
+ if ( '' === trim( (string) $request->get_body() ) && array() === $request->get_body_params() ) {
+ return null;
+ }
+
+ return self::error_response( $capability, 'VALIDATION_ERROR', __( 'UCP requests must be sent as application/json.', 'woocommerce' ), 'recoverable', 415 );
+ }
+
+ /**
+ * Read a parameter from the JSON document alone.
+ *
+ * `get_param()` would also resolve it from the form body or the query string. UCP defines
+ * the request as the document, so nothing carried elsewhere is allowed to act.
+ *
+ * @param \WP_REST_Request $request Request object.
+ * @param string $name Parameter name.
+ *
+ * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
+ *
+ * @return mixed|null The value, or null when the document has none.
+ */
+ public static function json_param( \WP_REST_Request $request, string $name ) {
+ $params = $request->get_json_params();
+
+ return is_array( $params ) && array_key_exists( $name, $params ) ? $params[ $name ] : null;
+ }
+
+ /**
+ * Translate a cart error code into the UCP code closest to it.
+ *
+ * The cart names more reasons than UCP does, so the ones an agent can act on
+ * differently — retry smaller, fix the quantity, or re-read the cart — stay apart
+ * and the rest collapse.
+ *
+ * @param string $code Store API error code.
+ * @param string $fallback UCP code for a cart error UCP has no closer name for.
+ * @return string One of `out_of_stock`, `not_found`, `invalid_quantity` or the fallback.
+ */
+ public static function cart_error_code( string $code, string $fallback = 'item_unavailable' ): string {
+ switch ( $code ) {
+ case 'woocommerce_rest_product_out_of_stock':
+ case 'woocommerce_rest_product_partially_out_of_stock':
+ case 'woocommerce_rest_product_too_many_in_cart':
+ return 'out_of_stock';
+ case 'woocommerce_rest_cart_invalid_key':
+ return 'not_found';
+ case 'woocommerce_rest_product_invalid_quantity':
+ case 'invalid_quantity':
+ case 'readonly_quantity':
+ return 'invalid_quantity';
+ default:
+ return $fallback;
+ }
+ }
+}
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/UcpUtils.php b/plugins/woocommerce/src/StoreApi/Utilities/UcpUtils.php
index 70b142d40d9..105c824a87c 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/UcpUtils.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/UcpUtils.php
@@ -3,204 +3,13 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\StoreApi\Utilities;
-use Automattic\WooCommerce\Utilities\FeaturesUtil;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
- * Shared helpers for the UCP (Universal Commerce Protocol) routes.
+ * Former name of the UCP helpers, kept so code written against it keeps loading.
+ *
+ * @deprecated since 11.4.0, use \Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils instead.
+ * @see Utils
*/
-class UcpUtils {
- /**
- * UCP specification version these routes implement.
- */
- const VERSION = '2026-04-08';
-
- /**
- * Feature id gating the UCP routes, stored as `woocommerce_feature_ucp_enabled`.
- */
- const FEATURE_ID = 'ucp';
-
- /**
- * Capability served by `POST /catalog/search`.
- */
- const CAPABILITY_CATALOG_SEARCH = 'dev.ucp.shopping.catalog.search';
-
- /**
- * Capability served by `POST /catalog/lookup` and `POST /catalog/product`.
- */
- const CAPABILITY_CATALOG_LOOKUP = 'dev.ucp.shopping.catalog.lookup';
-
- /**
- * Capability served by the `/carts` routes.
- */
- const CAPABILITY_CART = 'dev.ucp.shopping.cart';
-
- /**
- * Whether the UCP routes are registered.
- *
- * @return bool
- */
- public static function is_enabled(): bool {
- return FeaturesUtil::feature_is_enabled( self::FEATURE_ID );
- }
-
- /**
- * Build the `ucp` envelope every UCP response carries.
- *
- * UCP requires `capabilities` to name only the capability relevant to the
- * operation being answered, so each response declares exactly one.
- *
- * @param string $capability One of the CAPABILITY_* constants.
- * @param string $status Response status, `success` or `error`.
- * @return array
- */
- public static function response_metadata( string $capability, string $status ): array {
- return array(
- 'version' => self::VERSION,
- 'status' => $status,
- 'capabilities' => array(
- $capability => array( self::capability_declaration( $capability ) ),
- ),
- );
- }
-
- /**
- * Spec and schema references for every capability these routes serve, keyed by capability name.
- *
- * @return array
- */
- public static function capability_declarations(): array {
- return array(
- self::CAPABILITY_CATALOG_SEARCH => array(
- 'version' => self::VERSION,
- 'spec' => 'https://ucp.dev/' . self::VERSION . '/specification/catalog/search',
- 'schema' => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/catalog_search.json',
- ),
- self::CAPABILITY_CATALOG_LOOKUP => array(
- 'version' => self::VERSION,
- 'spec' => 'https://ucp.dev/' . self::VERSION . '/specification/catalog/lookup',
- 'schema' => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/catalog_lookup.json',
- ),
- self::CAPABILITY_CART => array(
- 'version' => self::VERSION,
- 'spec' => 'https://ucp.dev/' . self::VERSION . '/specification/cart',
- 'schema' => 'https://ucp.dev/' . self::VERSION . '/schemas/shopping/cart.json',
- ),
- );
- }
-
- /**
- * Spec and schema references for a capability.
- *
- * @param string $capability One of the CAPABILITY_* constants.
- * @return array
- */
- private static function capability_declaration( string $capability ): array {
- return self::capability_declarations()[ $capability ] ?? array( 'version' => self::VERSION );
- }
-
- /**
- * Build a UCP error response.
- *
- * The envelope shape every route shares. `$status` follows the UCP split: a malformed
- * request keeps a non-2xx status, a valid request that ends in a refusal is a business
- * outcome reported at 200 through `ucp.status` and `messages`.
- *
- * @param string $capability One of the CAPABILITY_* constants.
- * @param string $code Error code.
- * @param string $message Error message.
- * @param string $severity Error severity.
- * @param int $status HTTP status.
- * @param string $path JSONPath of the offending field.
- * @return \WP_REST_Response
- */
- public static function error_response( string $capability, string $code, string $message, string $severity, int $status, string $path = '$' ): \WP_REST_Response {
- return new \WP_REST_Response(
- array(
- 'ucp' => self::response_metadata( $capability, 'error' ),
- 'messages' => array(
- array(
- 'type' => 'error',
- 'code' => $code,
- 'content' => $message,
- 'severity' => $severity,
- 'path' => $path,
- 'content_type' => 'plain',
- ),
- ),
- ),
- $status
- );
- }
-
- /**
- * Refuse a request that brought a body or form fields under anything other than a JSON content type.
- *
- * A UCP request is a JSON document, so a form or query payload is never read in its
- * place. A bare request, such as a `cancel`, carries nothing and is not refused.
- *
- * @param \WP_REST_Request $request Request object.
- * @param string $capability One of the CAPABILITY_* constants.
- *
- * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
- *
- * @return \WP_REST_Response|null The 415 refusal, or null when the request may be read.
- */
- public static function refuse_non_json_payload( \WP_REST_Request $request, string $capability ): ?\WP_REST_Response {
- if ( $request->is_json_content_type() ) {
- return null;
- }
-
- if ( '' === trim( (string) $request->get_body() ) && array() === $request->get_body_params() ) {
- return null;
- }
-
- return self::error_response( $capability, 'VALIDATION_ERROR', __( 'UCP requests must be sent as application/json.', 'woocommerce' ), 'recoverable', 415 );
- }
-
- /**
- * Read a parameter from the JSON document alone.
- *
- * `get_param()` would also resolve it from the form body or the query string. UCP defines
- * the request as the document, so nothing carried elsewhere is allowed to act.
- *
- * @param \WP_REST_Request $request Request object.
- * @param string $name Parameter name.
- *
- * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
- *
- * @return mixed|null The value, or null when the document has none.
- */
- public static function json_param( \WP_REST_Request $request, string $name ) {
- $params = $request->get_json_params();
-
- return is_array( $params ) && array_key_exists( $name, $params ) ? $params[ $name ] : null;
- }
-
- /**
- * Translate a cart error code into the UCP code closest to it.
- *
- * The cart names more reasons than UCP does, so the ones an agent can act on
- * differently — retry smaller, fix the quantity, or re-read the cart — stay apart
- * and the rest collapse.
- *
- * @param string $code Store API error code.
- * @param string $fallback UCP code for a cart error UCP has no closer name for.
- * @return string One of `out_of_stock`, `not_found`, `invalid_quantity` or the fallback.
- */
- public static function cart_error_code( string $code, string $fallback = 'item_unavailable' ): string {
- switch ( $code ) {
- case 'woocommerce_rest_product_out_of_stock':
- case 'woocommerce_rest_product_partially_out_of_stock':
- case 'woocommerce_rest_product_too_many_in_cart':
- return 'out_of_stock';
- case 'woocommerce_rest_cart_invalid_key':
- return 'not_found';
- case 'woocommerce_rest_product_invalid_quantity':
- case 'invalid_quantity':
- case 'readonly_quantity':
- return 'invalid_quantity';
- default:
- return $fallback;
- }
- }
+class UcpUtils extends Utils {
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php b/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php
index d9298eeb7ba..6ca0f2e2dc9 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php
@@ -4,7 +4,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\Ucp;
use Automattic\WooCommerce\Internal\Ucp\UcpDiscovery;
-use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
+use Automattic\WooCommerce\StoreApi\Utilities\Ucp\Utils;
/**
* Tests for the `/.well-known/ucp` discovery profile.
@@ -40,15 +40,15 @@ class UcpDiscoveryTest extends \WC_Unit_Test_Case {
}
/**
- * @testdox The profile points at the local UCP REST namespace and lists every capability UcpUtils declares.
+ * @testdox The profile points at the local UCP REST namespace and lists every capability Utils declares.
*/
public function test_profile_shape(): void {
$profile = $this->sut->get_profile();
$this->assertSame( array( 'ucp', 'links' ), array_keys( $profile ) );
- $this->assertSame( UcpUtils::VERSION, $profile['ucp']['version'] );
+ $this->assertSame( Utils::VERSION, $profile['ucp']['version'] );
$this->assertSame( rest_url( 'wc/ucp/v1' ), $profile['ucp']['services']['dev.ucp.shopping'][0]['endpoint'] );
- $declarations = UcpUtils::capability_declarations();
+ $declarations = Utils::capability_declarations();
$this->assertSame( array_keys( $declarations ), array_keys( $profile['ucp']['capabilities'] ) );
foreach ( $declarations as $capability => $declaration ) {
$this->assertSame( array( $declaration ), $profile['ucp']['capabilities'][ $capability ] );
@@ -113,7 +113,7 @@ class UcpDiscoveryTest extends \WC_Unit_Test_Case {
$this->sut->maybe_flush_rewrite_rules( 'some_other_feature' );
$this->assertSame( 'no', get_option( 'woocommerce_queue_flush_rewrite_rules' ) );
- $this->sut->maybe_flush_rewrite_rules( UcpUtils::FEATURE_ID );
+ $this->sut->maybe_flush_rewrite_rules( Utils::FEATURE_ID );
$this->assertSame( 'yes', get_option( 'woocommerce_queue_flush_rewrite_rules' ) );
}