ProductsStore.php
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\SharedStores;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
use Automattic\WooCommerce\Blocks\Package;
use InvalidArgumentException;
/**
* Manages the registration of interactivity state that provides product data
* to interactive blocks. This is shared store data that is not tied to one
* specific block.
*
* This is an experimental API and may change in future versions.
*/
class ProductsStore {
/**
* The consent statement for using this experimental API.
*
* @var string
*/
private static string $consent_statement = 'I acknowledge that using experimental APIs means my theme or plugin will inevitably break in the next version of WooCommerce';
/**
* The namespace for the store.
*
* @var string
*/
private static string $store_namespace = 'woocommerce/products';
/**
* Products that have been loaded into state.
*
* @var array
*/
private static array $products = array();
/**
* Product variations that have been loaded into state.
*
* @var array
*/
private static array $product_variations = array();
/**
* Check that the consent statement was passed.
*
* @param string $consent_statement The consent statement string.
* @return true
* @throws InvalidArgumentException If the statement does not match.
*/
private static function check_consent( string $consent_statement ): bool {
if ( $consent_statement !== self::$consent_statement ) {
throw new InvalidArgumentException( 'This method cannot be called without consenting that the API may change.' );
}
return true;
}
/**
* Register the interactivity state if products have been loaded.
*
* @return void
*/
private static function register_state(): void {
$state = array();
if ( ! empty( self::$products ) ) {
$state['products'] = self::$products;
}
if ( ! empty( self::$product_variations ) ) {
$state['productVariations'] = self::$product_variations;
}
if ( ! empty( $state ) ) {
wp_interactivity_state( self::$store_namespace, $state );
}
}
/**
* Load a product into state.
*
* @param string $consent_statement The consent statement string.
* @param int $product_id The product ID.
* @return array The product data.
* @throws InvalidArgumentException If consent statement doesn't match.
*/
public static function load_product( string $consent_statement, int $product_id ): array {
self::check_consent( $consent_statement );
// Skip loading if product is already in state.
if ( isset( self::$products[ $product_id ] ) ) {
return self::$products[ $product_id ];
}
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data( '/wc/store/v1/products/' . $product_id );
self::$products[ $product_id ] = $response['body'] ?? array();
self::register_state();
return self::$products[ $product_id ];
}
/**
* Load all purchasable child products of a parent product into state.
*
* @param string $consent_statement The consent statement string.
* @param int $parent_id The parent product ID.
* @return array The purchasable child products keyed by ID.
* @throws InvalidArgumentException If consent statement doesn't match.
*/
public static function load_purchasable_child_products( string $consent_statement, int $parent_id ): array {
self::check_consent( $consent_statement );
// Get the parent product to retrieve child IDs.
$parent_product = wc_get_product( $parent_id );
if ( ! $parent_product ) {
return array();
}
// Get child product IDs (for grouped products, these are linked products).
$child_ids = $parent_product->get_children();
if ( empty( $child_ids ) ) {
return array();
}
// Query child products using include[] filter.
// The parent[] filter doesn't work for grouped products because
// their children are standalone products, not variations.
$include_params = array_map(
fn( $id ) => 'include[]=' . $id,
$child_ids
);
$query_string = implode( '&', $include_params );
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data( '/wc/store/v1/products?' . $query_string );
if ( empty( $response['body'] ) ) {
return array();
}
// Filter to only purchasable products.
$purchasable_products = array_filter(
$response['body'],
fn( $product ) => $product['is_purchasable']
);
// Re-key array by product ID and merge into state.
// Use array_replace instead of array_merge to preserve numeric keys.
$keyed_products = array_column( $purchasable_products, null, 'id' );
self::$products = array_replace( self::$products, $keyed_products );
self::register_state();
return $keyed_products;
}
/**
* Load all variations of a variable product into state.
*
* @param string $consent_statement The consent statement string.
* @param int $parent_id The parent product ID.
* @return array The variations keyed by ID.
* @throws InvalidArgumentException If consent statement doesn't match.
*/
public static function load_variations( string $consent_statement, int $parent_id ): array {
self::check_consent( $consent_statement );
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data( '/wc/store/v1/products?parent[]=' . $parent_id . '&type=variation' );
if ( empty( $response['body'] ) ) {
return array();
}
// Re-key array by variation ID and merge into state.
// Use array_replace instead of array_merge to preserve numeric keys.
$keyed_variations = array_column( $response['body'], null, 'id' );
self::$product_variations = array_replace( self::$product_variations, $keyed_variations );
self::register_state();
return $keyed_variations;
}
}