WC_Product_Variable::sync()   D
last analyzed

Complexity

Conditions 17
Paths 50

Size

Total Lines 106
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
dl 0
loc 106
rs 4.8361
c 0
b 0
f 0
eloc 58
nc 50
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
if ( ! defined( 'ABSPATH' ) ) {
4
	exit; // Exit if accessed directly
5
}
6
7
/**
8
 * Variable Product Class.
9
 *
10
 * The WooCommerce product class handles individual product data.
11
 *
12
 * @class 		WC_Product_Variable
13
 * @version		2.0.0
14
 * @package		WooCommerce/Classes/Products
15
 * @category	Class
16
 * @author 		WooThemes
17
 */
18
class WC_Product_Variable extends WC_Product {
19
20
	/** @public array Array of child products/posts/variations. */
21
	public $children = null;
22
23
	/** @private array Array of variation prices. */
24
	private $prices_array = array();
25
26
	/**
27
	 * Constructor.
28
	 *
29
	 * @param mixed $product
30
	 */
31
	public function __construct( $product ) {
32
		$this->product_type = 'variable';
33
		parent::__construct( $product );
34
	}
35
36
	/**
37
	 * Get the add to cart button text.
38
	 *
39
	 * @access public
40
	 * @return string
41
	 */
42
	public function add_to_cart_text() {
43
		return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Select options', 'woocommerce' ), $this );
44
	}
45
46
	/**
47
	 * Set stock level of the product.
48
	 *
49
	 * @param mixed $amount (default: null)
50
	 * @param string $mode can be set, add, or subtract
51
	 * @return int Stock
52
	 */
53
	public function set_stock( $amount = null, $mode = 'set' ) {
54
		$this->total_stock = '';
55
		delete_transient( 'wc_product_total_stock_' . $this->id . WC_Cache_Helper::get_transient_version( 'product' ) );
56
		return parent::set_stock( $amount, $mode );
57
	}
58
59
	/**
60
	 * Performed after a stock level change at product level.
61
	 */
62
	public function check_stock_status() {
63
		$set_child_stock_status = '';
64
65
		if ( ! $this->backorders_allowed() && $this->get_stock_quantity() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
66
			$set_child_stock_status = 'outofstock';
67
		} elseif ( $this->backorders_allowed() || $this->get_stock_quantity() > get_option( 'woocommerce_notify_no_stock_amount' ) ) {
68
			$set_child_stock_status = 'instock';
69
		}
70
71
		if ( $set_child_stock_status ) {
72
			foreach ( $this->get_children() as $child_id ) {
73
				if ( 'yes' !== get_post_meta( $child_id, '_manage_stock', true ) ) {
74
					wc_update_product_stock_status( $child_id, $set_child_stock_status );
75
				}
76
			}
77
78
			// Children statuses changed, so sync self
79
			self::sync_stock_status( $this->id );
80
		}
81
	}
82
83
	/**
84
	 * Set stock status.
85
	 */
86
	public function set_stock_status( $status ) {
87
		$status = 'outofstock' === $status ? 'outofstock' : 'instock';
88
89
		if ( update_post_meta( $this->id, '_stock_status', $status ) ) {
90
			do_action( 'woocommerce_product_set_stock_status', $this->id, $status );
91
		}
92
	}
93
94
	/**
95
	 * Return a products child ids.
96
	 *
97
	 * @param  boolean $visible_only Only return variations which are not hidden
98
	 * @return array of children ids
99
	 */
100
	public function get_children( $visible_only = false ) {
101
		$key            = $visible_only ? 'visible' : 'all';
102
		$transient_name = 'wc_product_children_' . $this->id;
103
104
		// Get value of transient
105
		if ( ! is_array( $this->children ) ) {
106
			$this->children = get_transient( $transient_name );
107
		}
108
109
		// Get value from DB
110
		if ( empty( $this->children ) || ! is_array( $this->children ) || ! isset( $this->children[ $key ] ) ) {
111
			$args = array(
112
				'post_parent' => $this->id,
113
				'post_type'   => 'product_variation',
114
				'orderby'     => 'menu_order',
115
				'order'       => 'ASC',
116
				'fields'      => 'ids',
117
				'post_status' => 'publish',
118
				'numberposts' => -1
119
			);
120
121
			if ( $visible_only ) {
122
				if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
123
					$args['meta_query'][] = array(
124
						'key'     => '_stock_status',
125
						'value'   => 'instock',
126
						'compare' => '=',
127
					);
128
				}
129
			}
130
131
			$args                   = apply_filters( 'woocommerce_variable_children_args', $args, $this, $visible_only );
132
			$this->children[ $key ] = get_posts( $args );
133
134
			set_transient( $transient_name, $this->children, DAY_IN_SECONDS * 30 );
135
		}
136
137
		return apply_filters( 'woocommerce_get_children', $this->children[ $key ], $this, $visible_only );
138
	}
139
140
	/**
141
	 * Get child product.
142
	 *
143
	 * @access public
144
	 * @param mixed $child_id
145
	 * @return WC_Product_Variation
146
	 */
147
	public function get_child( $child_id ) {
148
		return wc_get_product( $child_id, array(
149
			'parent_id' => $this->id,
150
			'parent' 	=> $this
151
		) );
152
	}
153
154
	/**
155
	 * Returns whether or not the product has any child product.
156
	 *
157
	 * @access public
158
	 * @return bool
159
	 */
160
	public function has_child() {
161
		return sizeof( $this->get_children() ) ? true : false;
162
	}
163
164
	/**
165
	 * Returns whether or not the product is on sale.
166
	 * @return bool
167
	 */
168
	public function is_on_sale() {
169
		$is_on_sale = false;
170
		$prices     = $this->get_variation_prices();
171
172
		if ( $prices['regular_price'] !== $prices['sale_price'] && $prices['sale_price'] === $prices['price'] ) {
173
			$is_on_sale = true;
174
		}
175
		return apply_filters( 'woocommerce_product_is_on_sale', $is_on_sale, $this );
176
	}
177
178
	/**
179
	 * Get the min or max variation regular price.
180
	 * @param  string $min_or_max - min or max
181
	 * @param  boolean  $display Whether the value is going to be displayed
182
	 * @return string
183
	 */
184
	public function get_variation_regular_price( $min_or_max = 'min', $display = false ) {
185
		$prices = $this->get_variation_prices( $display );
186
		$price  = 'min' === $min_or_max ? current( $prices['regular_price'] ) : end( $prices['regular_price'] );
187
		return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $display );
188
	}
189
190
	/**
191
	 * Get the min or max variation sale price.
192
	 * @param  string $min_or_max - min or max
193
	 * @param  boolean  $display Whether the value is going to be displayed
194
	 * @return string
195
	 */
196
	public function get_variation_sale_price( $min_or_max = 'min', $display = false ) {
197
		$prices = $this->get_variation_prices( $display );
198
		$price  = 'min' === $min_or_max ? current( $prices['sale_price'] ) : end( $prices['sale_price'] );
199
		return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $display );
200
	}
201
202
	/**
203
	 * Get the min or max variation (active) price.
204
	 * @param  string $min_or_max - min or max
205
	 * @param  boolean  $display Whether the value is going to be displayed
206
	 * @return string
207
	 */
208
	public function get_variation_price( $min_or_max = 'min', $display = false ) {
209
		$prices = $this->get_variation_prices( $display );
210
		$price  = 'min' === $min_or_max ? current( $prices['price'] ) : end( $prices['price'] );
211
		return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $display );
212
	}
213
214
	/**
215
	 * 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.
216
	 *
217
	 * 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.
218
	 * This is to ensure modified prices are not cached, unless intended.
219
	 *
220
	 * @param  bool $display Are prices for display? If so, taxes will be calculated.
221
	 * @return array() Array of RAW prices, regular prices, and sale prices with keys set to variation ID.
0 ignored issues
show
Documentation introduced by
The doc-type array() could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
222
	 */
223
	public function get_variation_prices( $display = false ) {
224
		global $wp_filter;
225
226
		/**
227
		 * Transient name for storing prices for this product (note: Max transient length is 45)
228
		 * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product.
229
		 */
230
		$transient_name = 'wc_var_prices_' . $this->id;
231
232
		/**
233
		 * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters.
234
		 * DEVELOPERS should filter this hash if offering conditonal pricing to keep it unique.
235
		 * @var string
236
		 */
237
		if ( $display ) {
238
			$price_hash = array( get_option( 'woocommerce_tax_display_shop', 'excl' ), WC_Tax::get_rates() );
239
		} else {
240
			$price_hash = array( false );
241
		}
242
243
		$filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' );
244
245
		foreach ( $filter_names as $filter_name ) {
246
			if ( ! empty( $wp_filter[ $filter_name ] ) ) {
247
				$price_hash[ $filter_name ] = array();
248
249
				foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) {
250
					$price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
251
				}
252
			}
253
		}
254
255
		$price_hash = md5( json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $this, $display ) ) );
256
257
		// If the value has already been generated, we don't need to grab the values again.
258
		if ( empty( $this->prices_array[ $price_hash ] ) ) {
259
260
			// Get value of transient
261
			$prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) );
262
263
			// If the product version has changed, reset cache
264
			if ( empty( $prices_array['version'] ) || $prices_array['version'] !== WC_Cache_Helper::get_transient_version( 'product' ) ) {
265
				$this->prices_array = array( 'version' => WC_Cache_Helper::get_transient_version( 'product' ) );
266
			}
267
268
			// If the prices are not stored for this hash, generate them
269
			if ( empty( $prices_array[ $price_hash ] ) ) {
270
				$prices         = array();
271
				$regular_prices = array();
272
				$sale_prices    = array();
273
				$variation_ids  = $this->get_children( true );
274
275
				foreach ( $variation_ids as $variation_id ) {
276
					if ( $variation = $this->get_child( $variation_id ) ) {
277
						$price         = apply_filters( 'woocommerce_variation_prices_price', $variation->price, $variation, $this );
278
						$regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->regular_price, $variation, $this );
279
						$sale_price    = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->sale_price, $variation, $this );
280
281
						// Skip empty prices
282
						if ( '' === $price ) {
283
							continue;
284
						}
285
286
						// If sale price does not equal price, the product is not yet on sale
287
						if ( $sale_price === $regular_price || $sale_price !== $price ) {
288
							$sale_price = $regular_price;
289
						}
290
291
						// If we are getting prices for display, we need to account for taxes
292
						if ( $display ) {
293
							if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
294
								$price         = '' === $price ? ''         : $variation->get_price_including_tax( 1, $price );
295
								$regular_price = '' === $regular_price ? '' : $variation->get_price_including_tax( 1, $regular_price );
296
								$sale_price    = '' === $sale_price ? ''    : $variation->get_price_including_tax( 1, $sale_price );
297
							} else {
298
								$price         = '' === $price ? ''         : $variation->get_price_excluding_tax( 1, $price );
299
								$regular_price = '' === $regular_price ? '' : $variation->get_price_excluding_tax( 1, $regular_price );
300
								$sale_price    = '' === $sale_price ? ''    : $variation->get_price_excluding_tax( 1, $sale_price );
301
							}
302
						}
303
304
						$prices[ $variation_id ]         = wc_format_decimal( $price, wc_get_price_decimals() );
305
						$regular_prices[ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() );
306
						$sale_prices[ $variation_id ]    = wc_format_decimal( $sale_price . '.00', wc_get_price_decimals() );
307
					}
308
				}
309
310
				asort( $prices );
311
				asort( $regular_prices );
312
				asort( $sale_prices );
313
314
				$prices_array[ $price_hash ] = array(
315
					'price'         => $prices,
316
					'regular_price' => $regular_prices,
317
					'sale_price'    => $sale_prices,
318
				);
319
320
				set_transient( $transient_name, json_encode( $prices_array ), DAY_IN_SECONDS * 30 );
321
			}
322
323
			/**
324
			 * Give plugins one last chance to filter the variation prices array which has been generated.
325
			 */
326
			$this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $prices_array[ $price_hash ], $this, $display );
327
		}
328
329
		/**
330
		 * Return the values.
331
		 */
332
		return $this->prices_array[ $price_hash ];
333
	}
334
335
	/**
336
	 * Returns the price in html format.
337
	 *
338
	 * @access public
339
	 * @param string $price (default: '')
340
	 * @return string
341
	 */
342
	public function get_price_html( $price = '' ) {
343
		$prices = $this->get_variation_prices( true );
344
345
		// No variations, or no active variation prices
346
		if ( $this->get_price() === '' || empty( $prices['price'] ) ) {
347
			$price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this );
348
		} else {
349
			$min_price = current( $prices['price'] );
350
			$max_price = end( $prices['price'] );
351
			$price     = $min_price !== $max_price ? sprintf( _x( '%1$s&ndash;%2$s', 'Price range: from-to', 'woocommerce' ), wc_price( $min_price ), wc_price( $max_price ) ) : wc_price( $min_price );
352
			$is_free   = $min_price == 0 && $max_price == 0;
353
354
			if ( $this->is_on_sale() ) {
355
				$min_regular_price = current( $prices['regular_price'] );
356
				$max_regular_price = end( $prices['regular_price'] );
357
				$regular_price     = $min_regular_price !== $max_regular_price ? sprintf( _x( '%1$s&ndash;%2$s', 'Price range: from-to', 'woocommerce' ), wc_price( $min_regular_price ), wc_price( $max_regular_price ) ) : wc_price( $min_regular_price );
358
				$price             = apply_filters( 'woocommerce_variable_sale_price_html', $this->get_price_html_from_to( $regular_price, $price ) . $this->get_price_suffix(), $this );
359
			} elseif ( $is_free ) {
360
				$price = apply_filters( 'woocommerce_variable_free_price_html', __( 'Free!', 'woocommerce' ), $this );
361
			} else {
362
				$price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this );
363
			}
364
		}
365
		return apply_filters( 'woocommerce_get_price_html', $price, $this );
366
	}
367
368
	/**
369
	 * Return an array of attributes used for variations, as well as their possible values.
370
	 *
371
	 * @return array of attributes and their available values
372
	 */
373
	public function get_variation_attributes() {
374
		global $wpdb;
375
376
		$variation_attributes = array();
377
		$attributes           = $this->get_attributes();
378
		$child_ids            = $this->get_children( true );
379
380
		if ( ! empty( $child_ids ) ) {
381
			foreach ( $attributes as $attribute ) {
382
				if ( empty( $attribute['is_variation'] ) ) {
383
					continue;
384
				}
385
386
				// Get possible values for this attribute, for only visible variations.
387
				$values = array_unique( $wpdb->get_col( $wpdb->prepare(
388
					"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN (" . implode( ',', array_map( 'esc_sql', $child_ids ) ) . ")",
389
					wc_variation_attribute_name( $attribute['name'] )
390
				) ) );
391
392
				// empty value indicates that all options for given attribute are available
393
				if ( in_array( '', $values ) ) {
394
					$values = $attribute['is_taxonomy'] ? wp_get_post_terms( $this->id, $attribute['name'], array( 'fields' => 'slugs' ) ) : wc_get_text_attributes( $attribute['value'] );
395
396
				// Get custom attributes (non taxonomy) as defined
397
				} elseif ( ! $attribute['is_taxonomy'] ) {
398
					$text_attributes          = wc_get_text_attributes( $attribute['value'] );
399
					$assigned_text_attributes = $values;
400
					$values                   = array();
401
402
					// Pre 2.4 handling where 'slugs' were saved instead of the full text attribute
403
					if ( version_compare( get_post_meta( $this->id, '_product_version', true ), '2.4.0', '<' ) ) {
404
						$assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes );
405
406
						foreach ( $text_attributes as $text_attribute ) {
407
							if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes ) ) {
408
								$values[] = $text_attribute;
409
							}
410
						}
411
					} else {
412
						foreach ( $text_attributes as $text_attribute ) {
413
							if ( in_array( $text_attribute, $assigned_text_attributes ) ) {
414
								$values[] = $text_attribute;
415
							}
416
						}
417
					}
418
				}
419
420
				$variation_attributes[ $attribute['name'] ] = array_unique( $values );
421
			}
422
		}
423
424
		return $variation_attributes;
425
	}
426
427
	/**
428
	 * If set, get the default attributes for a variable product.
429
	 *
430
	 * @access public
431
	 * @return array
432
	 */
433
	public function get_variation_default_attributes() {
434
		$default = isset( $this->default_attributes ) ? $this->default_attributes : '';
435
		return apply_filters( 'woocommerce_product_default_attributes', array_filter( (array) maybe_unserialize( $default ) ), $this );
436
	}
437
438
	/**
439
	 * Check if variable product has default attributes set.
440
	 *
441
	 * @access public
442
	 * @return bool
443
	 */
444
	public function has_default_attributes() {
445
		if ( ! $this->get_variation_default_attributes() ) {
446
			return true;
447
		}
448
		return false;
449
	}
450
451
	/**
452
	 * If set, get the default attributes for a variable product.
453
	 *
454
	 * @param string $attribute_name
455
	 * @return string
456
	 */
457
	public function get_variation_default_attribute( $attribute_name ) {
458
		$defaults       = $this->get_variation_default_attributes();
459
		$attribute_name = sanitize_title( $attribute_name );
460
		return isset( $defaults[ $attribute_name ] ) ? $defaults[ $attribute_name ] : '';
461
	}
462
463
	/**
464
	 * Match a variation to a given set of attributes using a WP_Query.
465
	 * @since  2.4.0
466
	 * @param  $match_attributes
467
	 * @return int Variation ID which matched, 0 is no match was found
468
	 */
469
	public function get_matching_variation( $match_attributes = array() ) {
470
		global $wpdb;
471
472
		$query_args = array(
473
			'post_parent' => $this->id,
474
			'post_type'   => 'product_variation',
475
			'orderby'     => 'menu_order',
476
			'order'       => 'ASC',
477
			'fields'      => 'ids',
478
			'post_status' => 'publish',
479
			'numberposts' => 1,
480
			'meta_query'  => array()
481
		);
482
483
		foreach ( $this->get_attributes() as $attribute ) {
484
			if ( ! $attribute['is_variation'] ) {
485
				continue;
486
			}
487
488
			$attribute_field_name = 'attribute_' . sanitize_title( $attribute['name'] );
489
490
			if ( ! isset( $match_attributes[ $attribute_field_name ] ) ) {
491
				return 0;
492
			}
493
494
			$value = wc_clean( $match_attributes[ $attribute_field_name ] );
495
496
			$query_args['meta_query'][] = array(
497
				'relation' => 'OR',
498
				array(
499
					'key'     => $attribute_field_name,
500
					'value'   => array( '', $value ),
501
					'compare' => 'IN'
502
				),
503
				array(
504
					'key'     => $attribute_field_name,
505
					'compare' => 'NOT EXISTS'
506
				)
507
			);
508
509
		}
510
511
		// Allow large queries in case user has many variations
512
		$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
513
514
		$matches = get_posts( $query_args );
515
516
		if ( $matches && ! is_wp_error( $matches ) ) {
517
			return current( $matches );
518
519
		/**
520
		 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
521
		 * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same. @see WC_Product_Variable::sync_attributes.
522
		 */
523
	 	} elseif ( version_compare( get_post_meta( $this->id, '_product_version', true ), '2.4.0', '<' ) ) {
524
			return $match_attributes === array_map( 'sanitize_title', $match_attributes ) ? 0 : $this->get_matching_variation( array_map( 'sanitize_title', $match_attributes ) );
525
526
		} else {
527
			return 0;
528
		}
529
	}
530
531
	/**
532
	 * Get an array of available variations for the current product.
533
	 * @return array
534
	 */
535
	public function get_available_variations() {
536
		$available_variations = array();
537
538
		foreach ( $this->get_children() as $child_id ) {
539
			$variation = $this->get_child( $child_id );
540
541
			// Hide out of stock variations if 'Hide out of stock items from the catalog' is checked
542
			if ( empty( $variation->variation_id ) || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) {
543
				continue;
544
			}
545
546
			// Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price)
547
			if ( apply_filters( 'woocommerce_hide_invisible_variations', false, $this->id, $variation ) && ! $variation->variation_is_visible() ) {
548
				continue;
549
			}
550
551
			$available_variations[] = $this->get_available_variation( $variation );
552
		}
553
554
		return $available_variations;
555
	}
556
557
	/**
558
	 * Returns an array of data for a variation. Used in the add to cart form.
559
	 * @since  2.4.0
560
	 * @param  WC_Product $variation Variation product object or ID
561
	 * @return array
562
	 */
563
	public function get_available_variation( $variation ) {
564
		if ( is_numeric( $variation ) ) {
565
			$variation = $this->get_child( $variation );
566
		}
567
568
		if ( has_post_thumbnail( $variation->get_variation_id() ) ) {
569
			$attachment_id     = get_post_thumbnail_id( $variation->get_variation_id() );
570
			$attachment        = wp_get_attachment_image_src( $attachment_id, 'shop_single' );
571
			$full_attachment   = wp_get_attachment_image_src( $attachment_id, 'full' );
572
			$attachment_object = get_post( $attachment_id );
573
			$image             = $attachment ? current( $attachment ) : '';
574
			$image_link        = $full_attachment ? current( $full_attachment ) : '';
575
			$image_title       = get_the_title( $attachment_id );
576
			$image_alt         = trim( strip_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
577
			$image_caption     = $attachment_object->post_excerpt;
578
			$image_srcset      = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, 'shop_single' ) : false;
579
			$image_sizes       = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, 'shop_single' ) : false;
580
581
			if ( empty( $image_alt ) ) {
582
				$image_alt = $image_title;
583
			}
584
		} else {
585
			$image = $image_link = $image_title = $image_alt = $image_srcset = $image_sizes = $image_caption = '';
586
		}
587
588
		$availability      = $variation->get_availability();
589
		$availability_html = empty( $availability['availability'] ) ? '' : '<p class="stock ' . esc_attr( $availability['class'] ) . '">' . wp_kses_post( $availability['availability'] ) . '</p>';
590
		$availability_html = apply_filters( 'woocommerce_stock_html', $availability_html, $availability['availability'], $variation );
591
592
		return apply_filters( 'woocommerce_available_variation', array(
593
			'variation_id'           => $variation->variation_id,
594
			'variation_is_visible'   => $variation->variation_is_visible(),
595
			'variation_is_active'    => $variation->variation_is_active(),
596
			'is_purchasable'         => $variation->is_purchasable(),
597
			'display_price'          => $variation->get_display_price(),
598
			'display_regular_price'  => $variation->get_display_price( $variation->get_regular_price() ),
599
			'attributes'             => $variation->get_variation_attributes(),
600
			'image_src'              => $image,
601
			'image_link'             => $image_link,
602
			'image_title'            => $image_title,
603
			'image_alt'              => $image_alt,
604
			'image_caption'          => $image_caption,
605
			'image_srcset'			 => $image_srcset ? $image_srcset : '',
606
			'image_sizes'			 => $image_sizes ? $image_sizes : '',
607
			'price_html'             => apply_filters( 'woocommerce_show_variation_price', $variation->get_price() === "" || $this->get_variation_price( 'min' ) !== $this->get_variation_price( 'max' ), $this, $variation ) ? '<span class="price">' . $variation->get_price_html() . '</span>' : '',
608
			'availability_html'      => $availability_html,
609
			'sku'                    => $variation->get_sku(),
610
			'weight'                 => $variation->get_weight() . ' ' . esc_attr( get_option('woocommerce_weight_unit' ) ),
611
			'dimensions'             => $variation->get_dimensions(),
612
			'min_qty'                => 1,
613
			'max_qty'                => $variation->backorders_allowed() ? '' : $variation->get_stock_quantity(),
614
			'backorders_allowed'     => $variation->backorders_allowed(),
615
			'is_in_stock'            => $variation->is_in_stock(),
616
			'is_downloadable'        => $variation->is_downloadable() ,
617
			'is_virtual'             => $variation->is_virtual(),
618
			'is_sold_individually'   => $variation->is_sold_individually() ? 'yes' : 'no',
619
			'variation_description'  => $variation->get_variation_description(),
620
		), $this, $variation );
621
	}
622
623
	/**
624
	 * Sync variable product prices with the children lowest/highest prices.
625
	 */
626
	public function variable_product_sync( $product_id = '' ) {
627
		if ( empty( $product_id ) ) {
628
			$product_id = $this->id;
629
		}
630
631
		// Sync prices with children
632
		self::sync( $product_id );
633
634
		// Re-load prices
635
		$this->price = get_post_meta( $product_id, '_price', true );
636
637
		foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) {
638
			$min_variation_id_key        = "min_{$price_type}_variation_id";
639
			$max_variation_id_key        = "max_{$price_type}_variation_id";
640
			$min_price_key               = "_min_variation_{$price_type}";
641
			$max_price_key               = "_max_variation_{$price_type}";
642
			$this->$min_variation_id_key = get_post_meta( $product_id, '_' . $min_variation_id_key, true );
643
			$this->$max_variation_id_key = get_post_meta( $product_id, '_' . $max_variation_id_key, true );
644
			$this->$min_price_key        = get_post_meta( $product_id, '_' . $min_price_key, true );
645
			$this->$max_price_key        = get_post_meta( $product_id, '_' . $max_price_key, true );
646
		}
647
	}
648
649
	/**
650
	 * Sync variable product stock status with children.
651
	 * @param  int $product_id
652
	 */
653
	public static function sync_stock_status( $product_id ) {
654
		$children = get_posts( array(
655
			'post_parent' 	=> $product_id,
656
			'posts_per_page'=> -1,
657
			'post_type' 	=> 'product_variation',
658
			'fields' 		=> 'ids',
659
			'post_status'	=> 'publish'
660
		) );
661
662
		$stock_status = 'outofstock';
663
664
		foreach ( $children as $child_id ) {
665
			$child_stock_status = get_post_meta( $child_id, '_stock_status', true );
666
			$child_stock_status = $child_stock_status ? $child_stock_status : 'instock';
667
			if ( 'instock' === $child_stock_status ) {
668
				$stock_status = 'instock';
669
				break;
670
			}
671
		}
672
673
		wc_update_product_stock_status( $product_id, $stock_status );
674
	}
675
676
	/**
677
	 * Sync the variable product's attributes with the variations.
678
	 */
679
	public static function sync_attributes( $product_id, $children = false ) {
680
		if ( ! $children ) {
681
			$children = get_posts( array(
682
				'post_parent' 	=> $product_id,
683
				'posts_per_page'=> -1,
684
				'post_type' 	=> 'product_variation',
685
				'fields' 		=> 'ids',
686
				'post_status'	=> 'any'
687
			) );
688
		}
689
690
		/**
691
		 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
692
		 * Attempt to get full version of the text attribute from the parent and UPDATE meta.
693
		 */
694
		if ( version_compare( get_post_meta( $product_id, '_product_version', true ), '2.4.0', '<' ) ) {
695
			$parent_attributes = array_filter( (array) get_post_meta( $product_id, '_product_attributes', true ) );
696
697
			foreach ( $children as $child_id ) {
698
				$all_meta = get_post_meta( $child_id );
699
700
				foreach ( $all_meta as $name => $value ) {
701
					if ( 0 !== strpos( $name, 'attribute_' ) ) {
702
						continue;
703
					}
704
					if ( sanitize_title( $value[0] ) === $value[0] ) {
705 View Code Duplication
						foreach ( $parent_attributes as $attribute ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
706
							if ( $name !== 'attribute_' . sanitize_title( $attribute['name'] ) ) {
707
								continue;
708
							}
709
							$text_attributes = wc_get_text_attributes( $attribute['value'] );
710
							foreach ( $text_attributes as $text_attribute ) {
711
								if ( sanitize_title( $text_attribute ) === $value[0] ) {
712
									update_post_meta( $child_id, $name, $text_attribute );
713
									break;
714
								}
715
							}
716
						}
717
					}
718
				}
719
			}
720
		}
721
	}
722
723
	/**
724
	 * Sync the variable product with it's children.
725
	 */
726
	public static function sync( $product_id ) {
727
		global $wpdb;
728
729
		$children = get_posts( array(
730
			'post_parent' 	=> $product_id,
731
			'posts_per_page'=> -1,
732
			'post_type' 	=> 'product_variation',
733
			'fields' 		=> 'ids',
734
			'post_status'	=> 'publish'
735
		) );
736
737
		// No published variations - product won't be purchasable.
738
		if ( ! $children ) {
739
			update_post_meta( $product_id, '_price', '' );
740
			delete_transient( 'wc_products_onsale' );
741
742
			if ( is_admin() && 'publish' === get_post_status( $product_id ) ) {
743
				WC_Admin_Meta_Boxes::add_error( __( 'This variable product has no active variations. Add or enable variations to allow this product to be purchased.', 'woocommerce' ) );
744
			}
745
746
		// Loop the variations
747
		} else {
748
749
			// Set the variable product to be virtual/downloadable if all children are virtual/downloadable
750
			foreach ( array( '_downloadable', '_virtual' ) as $meta_key ) {
751
				$all_variations_yes = true;
752
753
				foreach ( $children as $child_id ) {
754
					if ( 'yes' != get_post_meta( $child_id, $meta_key, true ) ) {
755
						$all_variations_yes = false;
756
						break;
757
					}
758
				}
759
760
				update_post_meta( $product_id, $meta_key, ( true === $all_variations_yes ) ? 'yes' : 'no' );
761
			}
762
763
			// Main active prices
764
			$min_price            = null;
765
			$max_price            = null;
766
			$min_price_id         = null;
767
			$max_price_id         = null;
768
769
			// Regular prices
770
			$min_regular_price    = null;
771
			$max_regular_price    = null;
772
			$min_regular_price_id = null;
773
			$max_regular_price_id = null;
774
775
			// Sale prices
776
			$min_sale_price       = null;
777
			$max_sale_price       = null;
778
			$min_sale_price_id    = null;
779
			$max_sale_price_id    = null;
780
781
			foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) {
782
				foreach ( $children as $child_id ) {
783
					$child_price = get_post_meta( $child_id, '_' . $price_type, true );
784
785
					// Skip non-priced variations
786
					if ( $child_price === '' ) {
787
						continue;
788
					}
789
790
					// Skip hidden variations
791
					if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
792
						$stock = get_post_meta( $child_id, '_stock', true );
793
						if ( $stock !== "" && $stock <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
794
							continue;
795
						}
796
					}
797
798
					// Find min price
799
					if ( is_null( ${"min_{$price_type}"} ) || $child_price < ${"min_{$price_type}"} ) {
800
						${"min_{$price_type}"}    = $child_price;
801
						${"min_{$price_type}_id"} = $child_id;
802
					}
803
804
					// Find max price
805
					if ( $child_price > ${"max_{$price_type}"} ) {
806
						${"max_{$price_type}"}    = $child_price;
807
						${"max_{$price_type}_id"} = $child_id;
808
					}
809
				}
810
811
				// Store prices
812
				update_post_meta( $product_id, '_min_variation_' . $price_type, ${"min_{$price_type}"} );
813
				update_post_meta( $product_id, '_max_variation_' . $price_type, ${"max_{$price_type}"} );
814
815
				// Store ids
816
				update_post_meta( $product_id, '_min_' . $price_type . '_variation_id', ${"min_{$price_type}_id"} );
817
				update_post_meta( $product_id, '_max_' . $price_type . '_variation_id', ${"max_{$price_type}_id"} );
818
			}
819
820
			// Sync _price meta
821
			delete_post_meta( $product_id, '_price' );
822
			add_post_meta( $product_id, '_price', $min_price, false );
823
			add_post_meta( $product_id, '_price', $max_price, false );
824
			delete_transient( 'wc_products_onsale' );
825
826
			// Sync attributes
827
			self::sync_attributes( $product_id, $children );
828
829
			do_action( 'woocommerce_variable_product_sync', $product_id, $children );
830
		}
831
	}
832
}
833