PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.4
WooCommerce v10.9.4
10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Blocks / SharedStores / ProductsStore.php
woocommerce / src / Blocks / SharedStores Last commit date
ProductsStore.php 2 months ago
ProductsStore.php
284 lines
1 <?php
2 declare(strict_types=1);
3
4 namespace Automattic\WooCommerce\Blocks\SharedStores;
5
6 use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
7 use Automattic\WooCommerce\Blocks\Package;
8 use InvalidArgumentException;
9
10 /**
11 * Shared store that hydrates the `woocommerce/products` Interactivity API
12 * store with product and variation data in Store API format.
13 *
14 * The store exposes two planes:
15 * - Raw data (`products`, `productVariations`) populated by the `load_*`
16 * methods below, each keyed by ID.
17 * - Selection (`productId`, `variationId`) — set by callers via
18 * `wp_interactivity_state` (global) or `data-wp-context` (per-element) —
19 * plus the derived getters (`mainProductInContext`,
20 * `productVariationInContext`, `productInContext`) registered by
21 * `register_getters()`.
22 *
23 * The derived getters are mirrored in the JS store
24 * (client/blocks/assets/js/base/stores/woocommerce/products.ts) so that
25 * directive bindings like `state.productInContext.sku` resolve during
26 * server-side rendering as well as on the client.
27 *
28 * See client/blocks/assets/js/base/stores/woocommerce/README.md for the
29 * full model and consumer examples.
30 *
31 * This is an experimental API and may change in future versions.
32 */
33 class ProductsStore {
34
35 /**
36 * The consent statement for using this experimental API.
37 *
38 * @var string
39 */
40 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';
41
42 /**
43 * The namespace for the store.
44 *
45 * @var string
46 */
47 private static string $store_namespace = 'woocommerce/products';
48
49 /**
50 * Products that have been loaded into state.
51 *
52 * @var array
53 */
54 private static array $products = array();
55
56 /**
57 * Product variations that have been loaded into state.
58 *
59 * @var array
60 */
61 private static array $product_variations = array();
62
63 /**
64 * Parent product IDs whose variations have already been loaded.
65 *
66 * @var array<int, true>
67 */
68 private static array $loaded_variation_parents = array();
69
70 /**
71 * Whether the derived-state getters have been registered.
72 *
73 * @var bool
74 */
75 private static bool $getters_registered = false;
76
77 /**
78 * Check that the consent statement was passed.
79 *
80 * @param string $consent_statement The consent statement string.
81 * @return true
82 * @throws InvalidArgumentException If the statement does not match.
83 */
84 private static function check_consent( string $consent_statement ): bool {
85 if ( $consent_statement !== self::$consent_statement ) {
86 throw new InvalidArgumentException( 'This method cannot be called without consenting that the API may change.' );
87 }
88
89 return true;
90 }
91
92 /**
93 * Register the derived-state getters once.
94 *
95 * These closures mirror the JS getters in
96 * client/blocks/assets/js/base/stores/woocommerce/products.ts so that
97 * directives referencing state.mainProductInContext /
98 * state.productVariationInContext / state.productInContext resolve
99 * during SSR. Because they read from
100 * wp_interactivity_state() at call time, they only need to be
101 * registered once regardless of how many products are added.
102 *
103 * @return void
104 */
105 private static function register_getters(): void {
106 if ( self::$getters_registered ) {
107 return;
108 }
109
110 self::$getters_registered = true;
111
112 wp_interactivity_state(
113 self::$store_namespace,
114 array(
115 'mainProductInContext' => function () {
116 $context = wp_interactivity_get_context();
117 $state = wp_interactivity_state( self::$store_namespace );
118 $product_id = array_key_exists( 'productId', $context )
119 ? $context['productId']
120 : ( $state['productId'] ?? null );
121
122 if ( ! $product_id ) {
123 return null;
124 }
125
126 return $state['products'][ $product_id ] ?? null;
127 },
128 'productVariationInContext' => function () {
129 $context = wp_interactivity_get_context();
130 $state = wp_interactivity_state( self::$store_namespace );
131 $variation_id = array_key_exists( 'variationId', $context )
132 ? $context['variationId']
133 : ( $state['variationId'] ?? null );
134
135 if ( ! $variation_id ) {
136 return null;
137 }
138
139 return $state['productVariations'][ $variation_id ] ?? null;
140 },
141 'productInContext' => function () {
142 $state = wp_interactivity_state( self::$store_namespace );
143 $selected = $state['productVariationInContext'] instanceof \Closure
144 ? $state['productVariationInContext']()
145 : $state['productVariationInContext'];
146
147 if ( $selected ) {
148 return $selected;
149 }
150
151 return $state['mainProductInContext'] instanceof \Closure
152 ? $state['mainProductInContext']()
153 : $state['mainProductInContext'];
154 },
155 )
156 );
157 }
158
159 /**
160 * Load a product into state.
161 *
162 * @param string $consent_statement The consent statement string.
163 * @param int $product_id The product ID.
164 * @return array The product data.
165 * @throws InvalidArgumentException If consent statement doesn't match.
166 */
167 public static function load_product( string $consent_statement, int $product_id ): array {
168 self::check_consent( $consent_statement );
169
170 // Skip loading if product is already in state.
171 if ( isset( self::$products[ $product_id ] ) ) {
172 return self::$products[ $product_id ];
173 }
174
175 $response = Package::container()->get( Hydration::class )->get_rest_api_response_data( '/wc/store/v1/products/' . $product_id );
176
177 self::$products[ $product_id ] = $response['body'] ?? array();
178 self::register_getters();
179 wp_interactivity_state(
180 self::$store_namespace,
181 array( 'products' => array( $product_id => self::$products[ $product_id ] ) )
182 );
183
184 return self::$products[ $product_id ];
185 }
186
187 /**
188 * Load all purchasable child products of a parent product into state.
189 *
190 * @param string $consent_statement The consent statement string.
191 * @param int $parent_id The parent product ID.
192 * @return array The purchasable child products keyed by ID.
193 * @throws InvalidArgumentException If consent statement doesn't match.
194 */
195 public static function load_purchasable_child_products( string $consent_statement, int $parent_id ): array {
196 self::check_consent( $consent_statement );
197
198 // Get the parent product to retrieve child IDs.
199 $parent_product = wc_get_product( $parent_id );
200 if ( ! $parent_product ) {
201 return array();
202 }
203
204 // Get child product IDs (for grouped products, these are linked products).
205 $child_ids = $parent_product->get_children();
206 if ( empty( $child_ids ) ) {
207 return array();
208 }
209
210 // Query child products using include[] filter.
211 // The parent[] filter doesn't work for grouped products because
212 // their children are standalone products, not variations.
213 $include_params = array_map(
214 fn( $id ) => 'include[]=' . $id,
215 $child_ids
216 );
217 $query_string = implode( '&', $include_params );
218
219 $response = Package::container()->get( Hydration::class )->get_rest_api_response_data( '/wc/store/v1/products?' . $query_string );
220
221 if ( empty( $response['body'] ) ) {
222 return array();
223 }
224
225 // Filter to only purchasable products.
226 $purchasable_products = array_filter(
227 $response['body'],
228 fn( $product ) => $product['is_purchasable']
229 );
230
231 // Re-key array by product ID and merge into state.
232 // Use array_replace instead of array_merge to preserve numeric keys.
233 $keyed_products = array_column( $purchasable_products, null, 'id' );
234 self::$products = array_replace( self::$products, $keyed_products );
235 self::register_getters();
236 wp_interactivity_state(
237 self::$store_namespace,
238 array( 'products' => $keyed_products )
239 );
240
241 return $keyed_products;
242 }
243
244 /**
245 * Load all variations of a variable product into state.
246 *
247 * @param string $consent_statement The consent statement string.
248 * @param int $parent_id The parent product ID.
249 * @return array The variations keyed by ID.
250 * @throws InvalidArgumentException If consent statement doesn't match.
251 */
252 public static function load_variations( string $consent_statement, int $parent_id ): array {
253 self::check_consent( $consent_statement );
254
255 // Skip loading if variations for this parent have already been loaded.
256 if ( isset( self::$loaded_variation_parents[ $parent_id ] ) ) {
257 return array_filter(
258 self::$product_variations,
259 fn( $variation ) => ( $variation['parent'] ?? 0 ) === $parent_id
260 );
261 }
262
263 $response = Package::container()->get( Hydration::class )->get_rest_api_response_data( '/wc/store/v1/products?parent[]=' . $parent_id . '&type=variation' );
264
265 self::$loaded_variation_parents[ $parent_id ] = true;
266
267 if ( empty( $response['body'] ) ) {
268 return array();
269 }
270
271 // Re-key array by variation ID and merge into state.
272 // Use array_replace instead of array_merge to preserve numeric keys.
273 $keyed_variations = array_column( $response['body'], null, 'id' );
274 self::$product_variations = array_replace( self::$product_variations, $keyed_variations );
275 self::register_getters();
276 wp_interactivity_state(
277 self::$store_namespace,
278 array( 'productVariations' => $keyed_variations )
279 );
280
281 return $keyed_variations;
282 }
283 }
284