CatalogProduct.php
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;
use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
/**
* UCP `POST /catalog/product` route.
*/
class CatalogProduct extends AbstractCatalogRoute {
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'ucp-catalog-product';
/**
* Maximum number of `selected` options honoured per product request.
*
* Every option is compared against every attribute of every variation.
*/
const MAX_SELECTED_OPTIONS = 25;
/**
* Get the path of this REST route.
*
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/catalog/product';
}
/**
* The UCP capability this route serves.
*
* @return string
*/
protected function get_capability(): string {
return UcpUtils::CAPABILITY_CATALOG_LOOKUP;
}
/**
* Request body arguments accepted by this route.
*
* `id` is deliberately not marked required: a missing id answers with the UCP
* `VALIDATION_ERROR` envelope rather than `rest_missing_callback_param`.
*
* @return array
*/
protected function get_request_args(): array {
return array_merge(
parent::get_request_args(),
array(
'id' => array(
'description' => __( 'Product or variant identifier.', 'woocommerce' ),
'type' => array( 'string', 'integer' ),
),
'selected' => array(
'description' => __( 'Option selections used to narrow the returned variants.', 'woocommerce' ),
'type' => 'array',
),
)
);
}
/**
* Handle the request.
*
* @param \WP_REST_Request $request Request object.
*
* @phpstan-param \WP_REST_Request<array<string, mixed>> $request
*
* @return \WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
$id = trim( strval( $request->get_param( 'id' ) ) );
if ( '' === $id ) {
return $this->ucp_error( 'VALIDATION_ERROR', __( 'id is required.', 'woocommerce' ), 'recoverable', 400 );
}
$selected = (array) ( $request->get_param( '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.
if ( count( $selected ) > self::MAX_SELECTED_OPTIONS ) {
return $this->ucp_error(
'request_too_large',
sprintf(
/* translators: %d: maximum number of selected options accepted per request. */
__( 'selected accepts at most %d options per request.', 'woocommerce' ),
self::MAX_SELECTED_OPTIONS
),
'recoverable',
400
);
}
$resolved = $this->resolve_product( $id );
if ( null === $resolved ) {
// A missing product is reported in the envelope, not as a 404: UCP
// clients read `ucp.status` and `messages`, not the HTTP status.
return new \WP_REST_Response(
array(
'ucp' => $this->ucp_metadata( 'error' ),
'messages' => array(
array(
'type' => 'error',
'code' => 'not_found',
'content' => __( 'Product not found.', 'woocommerce' ),
'severity' => 'unrecoverable',
'path' => '$.id',
'content_type' => 'plain',
),
),
),
200
);
}
$this->mapper()->begin_response();
$product = $this->mapper()->map_detail_product( $resolved['product'], $this->normalize_selected( $selected ), (int) $resolved['variation_id'] );
$response = array(
'ucp' => $this->ucp_metadata( 'success' ),
'product' => $product,
);
$messages = $this->truncation_messages( $this->mapper()->take_truncations(), array( $product ), '$.product' );
$currency_warning = $this->currency_warning( $this->get_context( $request ), $this->store_currency() );
if ( null !== $currency_warning ) {
array_unshift( $messages, $currency_warning );
}
if ( ! empty( $messages ) ) {
$response['messages'] = $messages;
}
return new \WP_REST_Response( $response, 200 );
}
/**
* Normalize selected options from the request payload.
*
* @param array $selected Raw selected options.
* @return array
*/
private function normalize_selected( array $selected ): array {
$normalized = array();
foreach ( $selected as $option ) {
if ( ! is_array( $option ) || ! isset( $option['name'], $option['label'] ) ) {
continue;
}
$entry = array(
'name' => strval( $option['name'] ),
'label' => strval( $option['label'] ),
);
if ( isset( $option['id'] ) ) {
$entry['id'] = strval( $option['id'] );
}
$normalized[] = $entry;
}
return $normalized;
}
}