WooCommerce Code Reference

ShopperListItemSchema.php

Source code

<?php
declare( strict_types = 1 );

namespace Automattic\WooCommerce\StoreApi\Schemas\V1;

use Automattic\WooCommerce\Internal\ShopperLists\ShopperListItem;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Utilities\ProductItemTrait;

/**
 * ShopperListItemSchema class.
 *
 * Serializes a {@see ShopperListItem}. Renders live product fields when the
 * item reports `is_live`, and falls back to at-save snapshot data otherwise.
 */
class ShopperListItemSchema extends AbstractSchema {
	// We only call format_variation_data(); see phpstan.neon for the related suppressions.
	use ProductItemTrait;

	/**
	 * The schema item name.
	 *
	 * @var string
	 */
	protected $title = 'shopper_list_item';

	/**
	 * The schema item identifier.
	 *
	 * @var string
	 */
	const IDENTIFIER = 'shopper-list-item';

	/**
	 * Image attachment schema instance.
	 *
	 * @var ImageAttachmentSchema
	 */
	protected $image_attachment_schema;

	/**
	 * Constructor.
	 *
	 * @throws \RuntimeException When the ImageAttachmentSchema is not registered.
	 *
	 * @param ExtendSchema     $extend Rest Extending instance.
	 * @param SchemaController $controller Schema Controller instance.
	 */
	public function __construct( ExtendSchema $extend, SchemaController $controller ) {
		parent::__construct( $extend, $controller );
		$schema = $this->controller->get( ImageAttachmentSchema::IDENTIFIER );
		if ( ! $schema instanceof ImageAttachmentSchema ) {
			throw new \RuntimeException( 'ImageAttachmentSchema is not registered in SchemaController.' );
		}
		$this->image_attachment_schema = $schema;
	}

	/**
	 * Item schema properties.
	 *
	 * @return array
	 */
	public function get_properties() {
		return array(
			'key'            => array(
				'description' => __( 'Stable identifier for the saved item within its list.', 'woocommerce' ),
				'type'        => 'string',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'id'             => array(
				'description' => __( 'Variation ID if applicable, otherwise product ID.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'product_id'     => array(
				'description' => __( 'Product ID at the time the item was saved.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'variation_id'   => array(
				'description' => __( 'Variation ID at the time the item was saved, or 0 for non-variable products.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'quantity'       => array(
				'description' => __( 'Quantity of this saved item.', 'woocommerce' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'is_live'        => array(
				'description' => __( 'True when the row serves live product data; false rows are at-save tombstones.', 'woocommerce' ),
				'type'        => 'boolean',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'is_purchasable' => array(
				'description' => __( 'True when the product can be added to the cart.', 'woocommerce' ),
				'type'        => 'boolean',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'name'           => array(
				'description' => __( 'Product name. Live when is_live is true; falls back to the at-save title snapshot otherwise.', 'woocommerce' ),
				'type'        => 'string',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'permalink'      => array(
				'description' => __( 'Product URL. Null when the row is a tombstone (so iAPI strips the anchor href).', 'woocommerce' ),
				'type'        => array( 'string', 'null' ),
				'format'      => 'uri',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'images'         => array(
				'description' => __( 'List of images for the live product. Empty when the product no longer exists.', 'woocommerce' ),
				'type'        => 'array',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'items'       => array(
					'type'       => 'object',
					'properties' => $this->image_attachment_schema->get_properties(),
				),
			),
			'variation'      => array(
				'description' => __( 'Chosen variation attributes, if applicable.', 'woocommerce' ),
				'type'        => 'array',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'items'       => array(
					'type'       => 'object',
					'properties' => array(
						'raw_attribute' => array(
							'description' => __( 'Variation system generated attribute name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'attribute'     => array(
							'description' => __( 'Variation attribute name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'value'         => array(
							'description' => __( 'Variation attribute value.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
					),
				),
			),
			'prices'         => array(
				'description' => __( 'Live product prices. Omitted when the product no longer exists.', 'woocommerce' ),
				'type'        => array( 'object', 'null' ),
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'properties'  => array_merge(
					$this->get_store_currency_properties(),
					array(
						'price'         => array(
							'description' => __( 'Current product price.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'regular_price' => array(
							'description' => __( 'Regular product price.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'sale_price'    => array(
							'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
					)
				),
			),
			'price_html'     => array(
				'description' => __( 'Live product price as HTML, formatted via wc_price including sale/discount markup. Empty when the product no longer exists.', 'woocommerce' ),
				'type'        => 'string',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'image_html'     => array(
				'description' => __( 'Product thumbnail as a fully-formed <img> element with srcset, sizes, alt, and lazy-loading attributes. Falls back to the configured placeholder image when the product has no image or no longer exists.', 'woocommerce' ),
				'type'        => 'string',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
			'date_added_gmt' => array(
				'description' => __( 'The date the item was saved, as GMT.', 'woocommerce' ),
				'type'        => 'string',
				'format'      => 'date-time',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
			),
		);
	}

	/**
	 * Serialize the saved item.
	 *
	 * @param ShopperListItem $item Saved item.
	 * @return array
	 */
	public function get_item_response( $item ) {
		$variation_id = $item->get_variation_id();
		$product_id   = $variation_id > 0 ? $variation_id : $item->get_product_id();
		$product      = $item->get_product();
		$is_live      = $item->is_live();

		$response = array(
			'key'            => $item->get_key(),
			'id'             => $product_id,
			'product_id'     => $item->get_product_id(),
			'variation_id'   => $variation_id,
			'quantity'       => $item->get_quantity(),
			'is_live'        => $is_live,
			'is_purchasable' => $item->is_purchasable(),
			'date_added_gmt' => wc_rest_prepare_date_response( $item->get_date_added_gmt() ),
		);

		if ( $is_live && $product instanceof \WC_Product ) {
			$response['name']       = $this->get_name( $product );
			$response['permalink']  = $product->get_permalink();
			$response['images']     = $this->get_images( $product );
			$response['variation']  = $this->format_variation_data( $item->get_variation_attributes(), $product );
			$response['prices']     = (object) $this->get_prices( $product );
			$response['price_html'] = (string) $product->get_price_html();
			$response['image_html'] = $this->get_image_html( $product );
		} else {
			$response['name']       = $this->prepare_html_response( $item->get_product_title_at_save() );
			$response['permalink']  = null;
			$response['images']     = array();
			$response['variation']  = array();
			$response['prices']     = null;
			$response['price_html'] = '';
			$response['image_html'] = $this->get_image_html( null );
		}

		return $response;
	}

	/**
	 * Get the displayable name for the live product.
	 *
	 * @param \WC_Product $product Live product instance.
	 * @return string
	 */
	private function get_name( \WC_Product $product ): string {
		$prepared = $this->prepare_html_response( $product->get_title() );
		return is_string( $prepared ) ? $prepared : (string) $product->get_title();
	}

	/**
	 * Get the main image for a shopper list item.
	 *
	 * Returns the product's main image only — shopper list rows are compact and
	 * the gallery isn't needed at the row level.
	 *
	 * @param \WC_Product $product Live product instance.
	 * @return array
	 */
	private function get_images( \WC_Product $product ): array {
		$image_id = (int) $product->get_image_id();
		if ( $image_id <= 0 ) {
			return array();
		}

		$image = $this->image_attachment_schema->get_item_response( $image_id );
		return $image ? array( $image ) : array();
	}

	/**
	 * Get the thumbnail image HTML for a shopper list item, falling back to the
	 * WooCommerce placeholder when the product has no image or has been deleted.
	 *
	 * Pre-formatting on the server lets renderers (PHP SSR + JS hydration)
	 * consume one canonical string instead of each side composing the markup
	 * from the structured `images` array. Mirrors the pattern WC uses in
	 * `ProductSchema::price_html` / `ProductImage::render`.
	 *
	 * @param \WC_Product|null $product Live product instance, or null for tombstones.
	 * @return string
	 */
	private function get_image_html( ?\WC_Product $product ): string {
		$image_id = $product instanceof \WC_Product ? (int) $product->get_image_id() : 0;
		if ( $image_id > 0 ) {
			return (string) wp_get_attachment_image( $image_id, 'woocommerce_thumbnail' );
		}
		return (string) wc_placeholder_img( 'woocommerce_thumbnail' );
	}

	/**
	 * Compute live prices for the saved item.
	 *
	 * We don't extend ProductSchema because saved items aren't products. The shape
	 * here is a thin subset of cart-item prices.
	 *
	 * @param \WC_Product $product Live product instance.
	 * @return array
	 */
	private function get_prices( \WC_Product $product ): array {
		$decimals      = wc_get_price_decimals();
		$regular_price = $product->get_regular_price();
		$sale_price    = $product->get_sale_price();
		$current_price = $product->get_price();

		return $this->prepare_currency_response(
			array(
				'price'         => $this->prepare_money_response( $current_price, $decimals ),
				'regular_price' => $this->prepare_money_response( '' === $regular_price ? $current_price : $regular_price, $decimals ),
				'sale_price'    => '' === $sale_price ? '' : $this->prepare_money_response( $sale_price, $decimals ),
			)
		);
	}
}