CatalogLookup.php
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\StoreApi\Routes\V1\Ucp;
use Automattic\WooCommerce\StoreApi\Utilities\UcpUtils;
/**
* UCP `POST /catalog/lookup` route.
*/
class CatalogLookup extends AbstractCatalogRoute {
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'ucp-catalog-lookup';
/**
* Maximum number of `ids` honoured per lookup request.
*
* Each id costs a product load plus a full transform on an unauthenticated
* endpoint. Unlike category filters, an over-long list is rejected rather than
* truncated: silently dropping requested ids would report existing products as
* missing.
*/
const MAX_LOOKUP_IDS = 50;
/**
* 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/lookup';
}
/**
* 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.
*
* `ids` is deliberately not marked required: a missing list 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(
'ids' => array(
'description' => __( 'Product or variant identifiers to look up.', 'woocommerce' ),
'type' => 'array',
'items' => array( 'type' => array( 'string', 'integer' ) ),
),
)
);
}
/**
* 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 ) {
$raw_ids = $request->get_param( 'ids' );
if ( ! is_array( $raw_ids ) || empty( $raw_ids ) ) {
return $this->ucp_error( 'VALIDATION_ERROR', __( 'ids is required.', 'woocommerce' ), 'recoverable', 400 );
}
// Deduplicate before counting: repeating one id is not a reason to reject,
// and each distinct id is what actually costs a product load.
$ids = array_values( array_unique( array_map( 'strval', $raw_ids ) ) );
if ( count( $ids ) > self::MAX_LOOKUP_IDS ) {
// `request_too_large` is the code UCP conformance names for a request
// over a batch size limit, which is exactly what an over-long id list is.
return $this->ucp_error(
'request_too_large',
sprintf(
/* translators: %d: maximum number of ids accepted per lookup request. */
__( 'ids accepts at most %d entries per request.', 'woocommerce' ),
self::MAX_LOOKUP_IDS
),
'recoverable',
400
);
}
$messages = array();
$currency_warning = $this->currency_warning( $this->get_context( $request ), $this->store_currency() );
if ( null !== $currency_warning ) {
$messages[] = $currency_warning;
}
// Several identifiers can resolve to the same product (a product id and one
// of its variation ids, say), so group them and emit one entry per product
// carrying every identifier that resolved to it.
$groups = array();
foreach ( $ids as $id ) {
$resolved = $this->resolve_product( $id );
if ( null === $resolved ) {
$messages[] = array(
'type' => 'info',
'code' => 'not_found',
'content' => sprintf(
/* translators: %s: product identifier. */
__( 'Product "%s" not found.', 'woocommerce' ),
$id
),
'path' => '$.ids',
'content_type' => 'plain',
);
continue;
}
$product_id = (int) $resolved['product']->get_id();
if ( ! isset( $groups[ $product_id ] ) ) {
$groups[ $product_id ] = array(
'product' => $resolved['product'],
'inputs' => array(),
);
}
$groups[ $product_id ]['inputs'][] = array(
'id' => $id,
'variation_id' => $resolved['variation_id'],
);
}
// One response spans several mapping calls here, so the per-response state
// is started explicitly rather than by the mapper.
$this->mapper()->begin_response();
$products = array();
foreach ( $groups as $group ) {
$products[] = $this->mapper()->map_lookup_product( $group['product'], $group['inputs'] );
}
$messages = array_merge(
$messages,
$this->truncation_messages( $this->mapper()->take_truncations(), $products, '$.products' )
);
$response = array(
'ucp' => $this->ucp_metadata( 'success' ),
'products' => $products,
);
if ( ! empty( $messages ) ) {
$response['messages'] = $messages;
}
return new \WP_REST_Response( $response, 200 );
}
}