Completed
Push — master ( 527840...a5d62b )
by Mike
53:46 queued 43:33
created

class-wc-product-variable-data-store-cpt.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * File for WC Variable Product Data Store class.
4
 *
5
 * @package WooCommerce/Classes
6
 */
7
8
if ( ! defined( 'ABSPATH' ) ) {
9
	exit;
10
}
11
12
/**
13
 * WC Variable Product Data Store: Stored in CPT.
14
 *
15
 * @version 3.0.0
16
 */
17
class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Product_Variable_Data_Store_Interface {
18
19
	/**
20
	 * Cached & hashed prices array for child variations.
21
	 *
22
	 * @var array
23
	 */
24
	protected $prices_array = array();
25
26
	/**
27
	 * Read attributes from post meta.
28
	 *
29
	 * @param WC_Product $product Product object.
30
	 */
31 60
	protected function read_attributes( &$product ) {
32 60
		$meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );
33
34 60
		if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
35 51
			$attributes   = array();
36 51
			$force_update = false;
37 51
			foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
38 51
				$meta_value = array_merge(
39
					array(
40 51
						'name'         => '',
41
						'value'        => '',
42
						'position'     => 0,
43
						'is_visible'   => 0,
44
						'is_variation' => 0,
45
						'is_taxonomy'  => 0,
46 51
					),
47
					(array) $meta_attribute_value
48
				);
49
50 51
				// Maintain data integrity. 4.9 changed sanitization functions - update the values here so variations function correctly.
51
				if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) {
52
					global $wpdb;
53
54
					$old_slug      = 'attribute_' . $meta_attribute_key;
55
					$new_slug      = 'attribute_' . sanitize_title( $meta_value['name'] );
56
					$old_meta_rows = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s;", $old_slug ) ); // WPCS: db call ok, cache ok.
57
58
					if ( $old_meta_rows ) {
59
						foreach ( $old_meta_rows as $old_meta_row ) {
60
							update_post_meta( $old_meta_row->post_id, $new_slug, $old_meta_row->meta_value );
61
						}
62
					}
63
64
					$force_update = true;
65
				}
66
67 51
				// Check if is a taxonomy attribute.
68 45 View Code Duplication
				if ( ! empty( $meta_value['is_taxonomy'] ) ) {
69
					if ( ! taxonomy_exists( $meta_value['name'] ) ) {
70
						continue;
71 45
					}
72 45
					$id      = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
73
					$options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
74 8
				} else {
75 8
					$id      = 0;
76
					$options = wc_get_text_attributes( $meta_value['value'] );
77
				}
78 51
79 51
				$attribute = new WC_Product_Attribute();
80 51
				$attribute->set_id( $id );
81 51
				$attribute->set_name( $meta_value['name'] );
82 51
				$attribute->set_options( $options );
83 51
				$attribute->set_position( $meta_value['position'] );
84 51
				$attribute->set_visible( $meta_value['is_visible'] );
85 51
				$attribute->set_variation( $meta_value['is_variation'] );
86
				$attributes[] = $attribute;
87 51
			}
88
			$product->set_attributes( $attributes );
89 51
90
			if ( $force_update ) {
91
				$this->update_attributes( $product, true );
92
			}
93
		}
94
	}
95
96
	/**
97
	 * Read product data.
98
	 *
99
	 * @param WC_Product $product Product object.
100
	 *
101
	 * @since 3.0.0
102 60
	 */
103 60
	protected function read_product_data( &$product ) {
104
		parent::read_product_data( $product );
105
106 60
		// Make sure data which does not apply to variables is unset.
107 60
		$product->set_regular_price( '' );
108
		$product->set_sale_price( '' );
109
	}
110
111
	/**
112
	 * Loads variation child IDs.
113
	 *
114
	 * @param WC_Product $product Product object.
115
	 * @param bool       $force_read True to bypass the transient.
116
	 *
117 59
	 * @return array
118 59
	 */
119 59
	public function read_children( &$product, $force_read = false ) {
120
		$children_transient_name = 'wc_product_children_' . $product->get_id();
121 59
		$children                = get_transient( $children_transient_name );
122
123 59
		if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) {
124 59
			$all_args = array(
125
				'post_parent' => $product->get_id(),
126
				'post_type'   => 'product_variation',
127
				'orderby'     => array(
128
					'menu_order' => 'ASC',
129 59
					'ID'         => 'ASC',
130
				),
131
				'fields'      => 'ids',
132
				'post_status' => array( 'publish', 'private' ),
133
				'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
134 59
			);
135 59
136
			$visible_only_args                = $all_args;
137 59
			$visible_only_args['post_status'] = 'publish';
138
139 View Code Duplication
			if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
140
				$visible_only_args['tax_query'][] = array(
141
					'taxonomy' => 'product_visibility',
142
					'field'    => 'name',
143
					'terms'    => 'outofstock',
144
					'operator' => 'NOT IN',
145 59
				);
146 59
			}
147
			$children['all']     = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) );
148 59
			$children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) );
149
150
			set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 );
151 59
		}
152 59
153
		$children['all']     = wp_parse_id_list( (array) $children['all'] );
154 59
		$children['visible'] = wp_parse_id_list( (array) $children['visible'] );
155
156
		return $children;
157
	}
158
159
	/**
160
	 * Loads an array of attributes used for variations, as well as their possible values.
161
	 *
162
	 * @param WC_Product $product Product object.
163 4
	 *
164
	 * @return array
165
	 */
166 4
	public function read_variation_attributes( &$product ) {
167 4
		global $wpdb;
168 4
169 4
		$variation_attributes = array();
170 4
		$attributes           = $product->get_attributes();
171 4
		$child_ids            = $product->get_children();
172
		$cache_key            = WC_Cache_Helper::get_cache_prefix( 'products' ) . 'product_variation_attributes_' . $product->get_id();
173 4
		$cache_group          = 'products';
174
		$cached_data          = wp_cache_get( $cache_key, $cache_group );
175
176
		if ( false !== $cached_data ) {
177 4
			return $cached_data;
178 4
		}
179 4
180
		if ( ! empty( $attributes ) ) {
181
			foreach ( $attributes as $attribute ) {
182
				if ( empty( $attribute['is_variation'] ) ) {
183
					continue;
184 4
				}
185 4
186 4
				// Get possible values for this attribute, for only visible variations.
187 4
				if ( ! empty( $child_ids ) ) {
188 4
					$format     = array_fill( 0, count( $child_ids ), '%d' );
189
					$query_in   = '(' . implode( ',', $format ) . ')';
190
					$query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids;
191
					$values     = array_unique(
192
						$wpdb->get_col(
193
							$wpdb->prepare( // wpcs: PreparedSQLPlaceholders replacement count ok.
194 4
								"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine.
195
								$query_args
196
							)
197 4
						)
198 1
					);
199 1
				} else {
200 1
					$values = array();
201
				}
202
203 1
				// Empty value indicates that all options for given attribute are available.
204
				if ( in_array( null, $values, true ) || in_array( '', $values, true ) || empty( $values ) ) {
205
					$values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] );
206
					// Get custom attributes (non taxonomy) as defined.
207
				} elseif ( ! $attribute['is_taxonomy'] ) {
208
					$text_attributes          = wc_get_text_attributes( $attribute['value'] );
209
					$assigned_text_attributes = $values;
210
					$values                   = array();
211 1
212 1
					// Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
213 1
					if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
214
						$assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes );
215
						foreach ( $text_attributes as $text_attribute ) {
216
							if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) {
217
								$values[] = $text_attribute;
218 4
							}
219
						}
220
					} else {
221
						foreach ( $text_attributes as $text_attribute ) {
222 4
							if ( in_array( $text_attribute, $assigned_text_attributes, true ) ) {
223
								$values[] = $text_attribute;
224 4
							}
225
						}
226
					}
227
				}
228
				$variation_attributes[ $attribute['name'] ] = array_unique( $values );
229
			}
230
		}
231
232
		wp_cache_set( $cache_key, $variation_attributes, $cache_group );
233
234
		return $variation_attributes;
235
	}
236
237
	/**
238 14
	 * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale.
239
	 *
240
	 * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter.
241
	 * This is to ensure modified prices are not cached, unless intended.
242
	 *
243
	 * @param WC_Product $product Product object.
244
	 * @param bool       $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
245 14
	 *
246
	 * @return array of prices
247 14
	 * @since  3.0.0
248
	 */
249
	public function read_price_data( &$product, $for_display = false ) {
250
251
		/**
252
		 * Transient name for storing prices for this product (note: Max transient length is 45)
253 14
		 *
254 14
		 * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product.
255
		 */
256
		$transient_name = 'wc_var_prices_' . $product->get_id();
257 14
258 14
		$price_hash = $this->get_price_hash( $product, $for_display );
259
260
		/**
261
		 * $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array.
262 14
		 * If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered.
263 14
		 */
264 14
		if ( empty( $this->prices_array[ $price_hash ] ) ) {
265 14
			$transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) );
266 14
267 14
			// If the product version has changed since the transient was last saved, reset the transient cache.
268 11
			if ( empty( $transient_cached_prices_array['version'] ) || WC_Cache_Helper::get_transient_version( 'product' ) !== $transient_cached_prices_array['version'] ) {
269
				$transient_cached_prices_array = array( 'version' => WC_Cache_Helper::get_transient_version( 'product' ) );
270 11
			}
271 11
272 11
			// If the prices are not stored for this hash, generate them and add to the transient.
273 11
			if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) {
274
				$prices_array = array(
275
					'price'         => array(),
276 11
					'regular_price' => array(),
277
					'sale_price'    => array(),
278
				);
279
280
				$variation_ids = $product->get_visible_children();
281 11
282 11
				if ( is_callable( '_prime_post_caches' ) ) {
283
					_prime_post_caches( $variation_ids );
284
				}
285
286 11
				foreach ( $variation_ids as $variation_id ) {
287 4
					$variation = wc_get_product( $variation_id );
288
289
					if ( $variation ) {
290
						$price         = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product );
291
						$regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product );
292
						$sale_price    = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product );
293
294
						// Skip empty prices.
295
						if ( '' === $price ) {
296
							continue;
297
						}
298
299
						// If sale price does not equal price, the product is not yet on sale.
300
						if ( $sale_price === $regular_price || $sale_price !== $price ) {
301
							$sale_price = $regular_price;
302
						}
303
304
						// If we are getting prices for display, we need to account for taxes.
305
						if ( $for_display ) {
306
							if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
307 4
								$price         = '' === $price ? '' : wc_get_price_including_tax(
308 4
									$variation,
309 4
									array(
310 4
										'qty'   => 1,
311
										'price' => $price,
312
									)
313 4
								);
314 4
								$regular_price = '' === $regular_price ? '' : wc_get_price_including_tax(
315 4
									$variation,
316 4
									array(
317
										'qty'   => 1,
318
										'price' => $regular_price,
319 4
									)
320 4
								);
321 4
								$sale_price    = '' === $sale_price ? '' : wc_get_price_including_tax(
322 4
									$variation,
323
									array(
324
										'qty'   => 1,
325
										'price' => $sale_price,
326
									)
327
								);
328 11
							} else {
329 11
								$price         = '' === $price ? '' : wc_get_price_excluding_tax(
330 11
									$variation,
331
									array(
332
										'qty'   => 1,
333
										'price' => $price,
334 14
									)
335 14
								);
336 14
								$regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax(
337 14
									$variation,
338
									array(
339
										'qty'   => 1,
340 14
										'price' => $regular_price,
341
									)
342
								);
343
								$sale_price    = '' === $sale_price ? '' : wc_get_price_excluding_tax(
344
									$variation,
345
									array(
346
										'qty'   => 1,
347 14
										'price' => $sale_price,
348
									)
349 14
								);
350
							}
351
						}
352
353
						$prices_array['price'][ $variation_id ]         = wc_format_decimal( $price, wc_get_price_decimals() );
354
						$prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() );
355
						$prices_array['sale_price'][ $variation_id ]    = wc_format_decimal( $sale_price . '.00', wc_get_price_decimals() );
356
357
						$prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display );
358
					}
359
				}
360
361 14
				// Add all pricing data to the transient array.
362
				foreach ( $prices_array as $key => $values ) {
363
					$transient_cached_prices_array[ $price_hash ][ $key ] = $values;
364 14
				}
365 14
366
				set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 );
367 14
			}
368 14
369
			/**
370
			 * Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class.
371
			 * This value may differ from the transient cache. It is filtered once before storing locally.
372 14
			 */
373
			$this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display );
374
		}
375
		return $this->prices_array[ $price_hash ];
376
	}
377 14
378 14
	/**
379 14
	 * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters.
380 14
	 * DEVELOPERS should filter this hash if offering conditional pricing to keep it unique.
381
	 *
382
	 * @param WC_Product $product Product object.
383
	 * @param bool       $for_display If taxes should be calculated or not.
384 14
	 *
385
	 * @since  3.0.0
386
	 * @return string
387
	 */
388
	protected function get_price_hash( &$product, $for_display = false ) {
389
		global $wp_filter;
390
391
		$price_hash   = $for_display && wc_tax_enabled() ? array( get_option( 'woocommerce_tax_display_shop', 'excl' ), WC_Tax::get_rates() ) : array( false );
392
		$filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' );
393
394
		foreach ( $filter_names as $filter_name ) {
395
			if ( ! empty( $wp_filter[ $filter_name ] ) ) {
396
				$price_hash[ $filter_name ] = array();
397
398
				foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) {
399
					$price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
400
				}
401
			}
402
		}
403
404
		$price_hash[] = WC_Cache_Helper::get_transient_version( 'product' );
405
		$price_hash   = md5(
406
			wp_json_encode(
407 2
				apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display )
408
			)
409 2
		);
410 2
411
		return $price_hash;
412
	}
413
414
	/**
415
	 * Does a child have a weight set?
416
	 *
417
	 * @param WC_Product $product Product object.
418
	 *
419
	 * @since  3.0.0
420 60
	 * @return boolean
421 60
	 */
422 View Code Duplication
	public function child_has_weight( $product ) {
423
		global $wpdb;
424
		$children = $product->get_visible_children();
425
		if ( ! $children ) {
426
			return false;
427
		}
428
429
		$format   = array_fill( 0, count( $children ), '%d' );
430
		$query_in = '(' . implode( ',', $format ) . ')';
431
432 60
		return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine.
433
	}
434
435 60
	/**
436
	 * Does a child have dimensions set?
437 60
	 *
438 9
	 * @param WC_Product $product Product object.
439 9
	 *
440 9
	 * @since  3.0.0
441 9
	 * @return boolean
442
	 */
443 View Code Duplication
	public function child_has_dimensions( $product ) {
444
		global $wpdb;
445 60
		$children = $product->get_visible_children();
446
		if ( ! $children ) {
447
			return false;
448 60
		}
449
450
		$format   = array_fill( 0, count( $children ), '%d' );
451
		$query_in = '(' . implode( ',', $format ) . ')';
452
453
		return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine.
454
	}
455
456
	/**
457
	 * Is a child in stock?
458
	 *
459 60
	 * @param WC_Product $product Product object.
460 60
	 *
461
	 * @since  3.0.0
462
	 * @return boolean
463 51
	 */
464 51
	public function child_is_in_stock( $product ) {
465 51
		return $this->child_has_stock_status( $product, 'instock' );
466
	}
467
468
	/**
469 51
	 * Does a child have a stock status?
470 51
	 *
471 51
	 * @param WC_Product $product Product object.
472
	 * @param string     $status 'instock', 'outofstock', or 'onbackorder'.
473
	 *
474
	 * @since  3.3.0
475
	 * @return boolean
476
	 */
477
	public function child_has_stock_status( $product, $status ) {
478
		global $wpdb;
479
480
		$children = $product->get_children();
481
482
		if ( $children ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $children of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
483
			$format               = array_fill( 0, count( $children ), '%d' );
484 60
			$query_in             = '(' . implode( ',', $format ) . ')';
485
			$query_args           = array( 'stock_status' => $status ) + $children;
486
			$children_with_status = $wpdb->get_var(
487 60
				$wpdb->prepare( // wpcs: PreparedSQLPlaceholders replacement count ok.
488 3
					"SELECT COUNT( post_id ) FROM $wpdb->postmeta WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine.
489 3
					$query_args
490 3
				)
491 3
			);
492 3
		} else {
493 2
			$children_with_status = 0;
494 2
		}
495
496
		return (bool) $children_with_status;
497 3
	}
498 2
499 2
	/**
500 2
	 * Syncs all variation names if the parent name is changed.
501
	 *
502
	 * @param WC_Product $product Product object.
503
	 * @param string     $previous_name Variation previous name.
504
	 * @param string     $new_name Variation new name.
505
	 *
506
	 * @since 3.0.0
507
	 */
508
	public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) {
509
		if ( $new_name !== $previous_name ) {
510
			global $wpdb;
511 1
512
			$wpdb->query(
513
				$wpdb->prepare(
514 1
					"UPDATE {$wpdb->posts}
515 1
					SET post_title = REPLACE( post_title, %s, %s )
516
					WHERE post_type = 'product_variation'
517 1
					AND post_parent = %d",
518 1
					$previous_name ? $previous_name : 'AUTO-DRAFT',
519 1
					$new_name,
520
					$product->get_id()
521 1
				)
522
			);
523
		}
524
	}
525
526
	/**
527
	 * Stock managed at the parent level - update children being managed by this product.
528
	 * This sync function syncs downwards (from parent to child) when the variable product is saved.
529
	 *
530
	 * @param WC_Product $product Product object.
531
	 *
532
	 * @since 3.0.0
533
	 */
534
	public function sync_managed_variation_stock_status( &$product ) {
535
		global $wpdb;
536
537
		if ( $product->get_manage_stock() ) {
538
			$children = $product->get_children();
539
			$changed  = false;
540 60
541 60
			if ( $children ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $children of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
542 9
				$status           = $product->get_stock_status();
543 60
				$format           = array_fill( 0, count( $children ), '%d' );
544 1
				$query_in         = '(' . implode( ',', $format ) . ')';
545
				$managed_children = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine.
546 60
				foreach ( $managed_children as $managed_child ) {
547
					if ( update_post_meta( $managed_child, '_stock_status', $status ) ) {
548
						$changed = true;
549
					}
550
				}
551
			}
552
553
			if ( $changed ) {
554
				$children = $this->read_children( $product, true );
555
				$product->set_children( $children['all'] );
556
				$product->set_visible_children( $children['visible'] );
557 9
			}
558 9
		}
559
	}
560
561
	/**
562 9
	 * Sync variable product prices with children.
563 9
	 *
564
	 * @param WC_Product $product Product object.
565 9
	 *
566 9
	 * @since 3.0.0
567 9
	 */
568
	public function sync_price( &$product ) {
569
		global $wpdb;
570
571
		$children = $product->get_visible_children();
572
		if ( $children ) {
573
			$format   = array_fill( 0, count( $children ), '%d' );
574 9
			$query_in = '(' . implode( ',', $format ) . ')';
575 2
			$prices   = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine.
576 2
		} else {
577 2
			$prices = array();
578
		}
579 2
580
		delete_post_meta( $product->get_id(), '_price' );
581
		delete_post_meta( $product->get_id(), '_sale_price' );
582
		delete_post_meta( $product->get_id(), '_regular_price' );
583
584 9
		if ( $prices ) {
585
			sort( $prices );
586
			// To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner.
587
			foreach ( $prices as $price ) {
588
				if ( is_null( $price ) || '' === $price ) {
589
					continue;
590
				}
591
				add_post_meta( $product->get_id(), '_price', $price, false );
592
			}
593
		}
594
	}
595
596
	/**
597
	 * Sync variable product stock status with children.
598
	 * Change does not persist unless saved by caller.
599
	 *
600
	 * @param WC_Product $product Product object.
601
	 *
602
	 * @since 3.0.0
603
	 */
604
	public function sync_stock_status( &$product ) {
605
		if ( $product->child_is_in_stock() ) {
606
			$product->set_stock_status( 'instock' );
607
		} elseif ( $product->child_is_on_backorder() ) {
608
			$product->set_stock_status( 'onbackorder' );
609
		} else {
610
			$product->set_stock_status( 'outofstock' );
611
		}
612
	}
613
614
	/**
615
	 * Delete variations of a product.
616
	 *
617
	 * @param int  $product_id Product ID.
618
	 * @param bool $force_delete False to trash.
619
	 *
620
	 * @since 3.0.0
621
	 */
622
	public function delete_variations( $product_id, $force_delete = false ) {
623
		if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
624
			return;
625
		}
626
627
		$variation_ids = wp_parse_id_list(
628
			get_posts(
629
				array(
630
					'post_parent' => $product_id,
631
					'post_type'   => 'product_variation',
632
					'fields'      => 'ids',
633
					'post_status' => array( 'any', 'trash', 'auto-draft' ),
634
					'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
635
				)
636
			)
637
		);
638
639
		if ( ! empty( $variation_ids ) ) {
640
			foreach ( $variation_ids as $variation_id ) {
641
				if ( $force_delete ) {
642
					wp_delete_post( $variation_id, true );
643
				} else {
644
					wp_trash_post( $variation_id );
645
				}
646
			}
647
		}
648
649
		delete_transient( 'wc_product_children_' . $product_id );
650
	}
651
652
	/**
653
	 * Untrash variations.
654
	 *
655
	 * @param int $product_id Product ID.
656
	 */
657
	public function untrash_variations( $product_id ) {
658
		$variation_ids = wp_parse_id_list(
659
			get_posts(
660
				array(
661
					'post_parent' => $product_id,
662
					'post_type'   => 'product_variation',
663
					'fields'      => 'ids',
664
					'post_status' => 'trash',
665
					'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
666
				)
667
			)
668
		);
669
670
		if ( ! empty( $variation_ids ) ) {
671
			foreach ( $variation_ids as $variation_id ) {
672
				wp_untrash_post( $variation_id );
673
			}
674
		}
675
676
		delete_transient( 'wc_product_children_' . $product_id );
677
	}
678
}
679