Completed
Pull Request — master (#11615)
by Mike
11:15
created

WC_Product_Variable::child_has_weight()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
b 0
f 0
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
		$text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Select options', 'woocommerce' ) : __( 'Read more', 'woocommerce' );
44
45
		return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this );
46
	}
47
48
	/**
49
	 * Set stock level of the product.
50
	 *
51
	 * @param mixed $amount (default: null)
52
	 * @param string $mode can be set, add, or subtract
53
	 * @return int Stock
54
	 */
55
	public function set_stock( $amount = null, $mode = 'set' ) {
56
		$this->total_stock = '';
57
		delete_transient( 'wc_product_total_stock_' . $this->id . WC_Cache_Helper::get_transient_version( 'product' ) );
58
		return parent::set_stock( $amount, $mode );
59
	}
60
61
	/**
62
	 * Performed after a stock level change at product level.
63
	 */
64
	public function check_stock_status() {
65
		$set_child_stock_status = '';
66
67
		if ( ! $this->backorders_allowed() && $this->get_stock_quantity() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
68
			$set_child_stock_status = 'outofstock';
69
		} elseif ( $this->backorders_allowed() || $this->get_stock_quantity() > get_option( 'woocommerce_notify_no_stock_amount' ) ) {
70
			$set_child_stock_status = 'instock';
71
		}
72
73
		if ( $set_child_stock_status ) {
74
			foreach ( $this->get_children() as $child_id ) {
75
				if ( 'yes' !== get_post_meta( $child_id, '_manage_stock', true ) ) {
76
					wc_update_product_stock_status( $child_id, $set_child_stock_status );
77
				}
78
			}
79
80
			// Children statuses changed, so sync self
81
			self::sync_stock_status( $this->id );
82
		}
83
	}
84
85
	/**
86
	 * Set stock status.
87
	 */
88
	public function set_stock_status( $status ) {
89
		$status = 'outofstock' === $status ? 'outofstock' : 'instock';
90
91
		if ( update_post_meta( $this->id, '_stock_status', $status ) ) {
92
			do_action( 'woocommerce_product_set_stock_status', $this->id, $status );
93
		}
94
	}
95
96
	/**
97
	 * Return a products child ids.
98
	 *
99
	 * @param  boolean $visible_only Only return variations which are not hidden
100
	 * @return array of children ids
101
	 */
102
	public function get_children( $visible_only = false ) {
103
		$key            = $visible_only ? 'visible' : 'all';
104
		$transient_name = 'wc_product_children_' . $this->id;
105
106
		// Get value of transient
107
		if ( ! is_array( $this->children ) ) {
108
			$this->children = get_transient( $transient_name );
109
		}
110
111
		// Get value from DB
112
		if ( empty( $this->children ) || ! is_array( $this->children ) || ! isset( $this->children[ $key ] ) ) {
113
			$args = array(
114
				'post_parent' => $this->id,
115
				'post_type'   => 'product_variation',
116
				'orderby'     => 'menu_order',
117
				'order'       => 'ASC',
118
				'fields'      => 'ids',
119
				'post_status' => 'publish',
120
				'numberposts' => -1
121
			);
122
123
			if ( $visible_only ) {
124
				if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
125
					$args['meta_query'][] = array(
126
						'key'     => '_stock_status',
127
						'value'   => 'instock',
128
						'compare' => '=',
129
					);
130
				}
131
			}
132
133
			$args                   = apply_filters( 'woocommerce_variable_children_args', $args, $this, $visible_only );
134
			$this->children[ $key ] = get_posts( $args );
135
136
			set_transient( $transient_name, $this->children, DAY_IN_SECONDS * 30 );
137
		}
138
139
		return apply_filters( 'woocommerce_get_children', $this->children[ $key ], $this, $visible_only );
140
	}
141
142
	/**
143
	 * Get child product.
144
	 *
145
	 * @access public
146
	 * @param mixed $child_id
147
	 * @return WC_Product_Variation
148
	 */
149
	public function get_child( $child_id ) {
150
		return wc_get_product( $child_id, array(
151
			'parent_id' => $this->id,
152
			'parent' 	=> $this
153
		) );
154
	}
155
156
	/**
157
	 * Returns whether or not the product has any child product.
158
	 *
159
	 * @access public
160
	 * @return bool
161
	 */
162
	public function has_child() {
163
		return sizeof( $this->get_children() ) ? true : false;
164
	}
165
166
	/**
167
	 * Returns whether or not the product is on sale.
168
	 * @return bool
169
	 */
170
	public function is_on_sale() {
171
		$is_on_sale = false;
172
		$prices     = $this->get_variation_prices();
173
174
		if ( $prices['regular_price'] !== $prices['sale_price'] && $prices['sale_price'] === $prices['price'] ) {
175
			$is_on_sale = true;
176
		}
177
		return apply_filters( 'woocommerce_product_is_on_sale', $is_on_sale, $this );
178
	}
179
180
	/**
181
	 * Get the min or max variation regular price.
182
	 * @param  string $min_or_max - min or max
183
	 * @param  boolean  $display Whether the value is going to be displayed
184
	 * @return string
185
	 */
186
	public function get_variation_regular_price( $min_or_max = 'min', $display = false ) {
187
		$prices = $this->get_variation_prices( $display );
188
		$price  = 'min' === $min_or_max ? current( $prices['regular_price'] ) : end( $prices['regular_price'] );
189
		return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $display );
190
	}
191
192
	/**
193
	 * Get the min or max variation sale price.
194
	 * @param  string $min_or_max - min or max
195
	 * @param  boolean  $display Whether the value is going to be displayed
196
	 * @return string
197
	 */
198
	public function get_variation_sale_price( $min_or_max = 'min', $display = false ) {
199
		$prices = $this->get_variation_prices( $display );
200
		$price  = 'min' === $min_or_max ? current( $prices['sale_price'] ) : end( $prices['sale_price'] );
201
		return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $display );
202
	}
203
204
	/**
205
	 * Get the min or max variation (active) price.
206
	 * @param  string $min_or_max - min or max
207
	 * @param  boolean  $display Whether the value is going to be displayed
208
	 * @return string
209
	 */
210
	public function get_variation_price( $min_or_max = 'min', $display = false ) {
211
		$prices = $this->get_variation_prices( $display );
212
		$price  = 'min' === $min_or_max ? current( $prices['price'] ) : end( $prices['price'] );
213
		return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $display );
214
	}
215
216
	/**
217
	 * 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.
218
	 *
219
	 * 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.
220
	 * This is to ensure modified prices are not cached, unless intended.
221
	 *
222
	 * @param  bool $display Are prices for display? If so, taxes will be calculated.
223
	 * @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...
224
	 */
225
	public function get_variation_prices( $display = false ) {
226
		global $wp_filter;
227
228
		/**
229
		 * Transient name for storing prices for this product (note: Max transient length is 45)
230
		 * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product.
231
		 */
232
		$transient_name = 'wc_var_prices_' . $this->id;
233
234
		/**
235
		 * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters.
236
		 * DEVELOPERS should filter this hash if offering conditonal pricing to keep it unique.
237
		 * @var string
238
		 */
239
		if ( $display ) {
240
			$price_hash = array( get_option( 'woocommerce_tax_display_shop', 'excl' ), WC_Tax::get_rates() );
241
		} else {
242
			$price_hash = array( false );
243
		}
244
245
		$filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' );
246
247
		foreach ( $filter_names as $filter_name ) {
248
			if ( ! empty( $wp_filter[ $filter_name ] ) ) {
249
				$price_hash[ $filter_name ] = array();
250
251
				foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) {
252
					$price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
253
				}
254
			}
255
		}
256
257
		$price_hash[] = WC_Cache_Helper::get_transient_version( 'product' );
258
		$price_hash   = md5( json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $this, $display ) ) );
259
260
		// If the value has already been generated, we don't need to grab the values again.
261
		if ( empty( $this->prices_array[ $price_hash ] ) ) {
262
263
			// Get value of transient
264
			$prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) );
265
266
			// If the product version has changed, reset cache
267
			if ( empty( $prices_array['version'] ) || $prices_array['version'] !== WC_Cache_Helper::get_transient_version( 'product' ) ) {
268
				$this->prices_array = array( 'version' => WC_Cache_Helper::get_transient_version( 'product' ) );
269
			}
270
271
			// If the prices are not stored for this hash, generate them
272
			if ( empty( $prices_array[ $price_hash ] ) ) {
273
				$prices         = array();
274
				$regular_prices = array();
275
				$sale_prices    = array();
276
				$variation_ids  = $this->get_children( true );
277
278
				foreach ( $variation_ids as $variation_id ) {
279
					if ( $variation = $this->get_child( $variation_id ) ) {
280
						$price         = apply_filters( 'woocommerce_variation_prices_price', $variation->price, $variation, $this );
281
						$regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->regular_price, $variation, $this );
282
						$sale_price    = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->sale_price, $variation, $this );
283
284
						// Skip empty prices
285
						if ( '' === $price ) {
286
							continue;
287
						}
288
289
						// If sale price does not equal price, the product is not yet on sale
290
						if ( $sale_price === $regular_price || $sale_price !== $price ) {
291
							$sale_price = $regular_price;
292
						}
293
294
						// If we are getting prices for display, we need to account for taxes
295
						if ( $display ) {
296
							if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
297
								$price         = '' === $price ? ''         : $variation->get_price_including_tax( 1, $price );
298
								$regular_price = '' === $regular_price ? '' : $variation->get_price_including_tax( 1, $regular_price );
299
								$sale_price    = '' === $sale_price ? ''    : $variation->get_price_including_tax( 1, $sale_price );
300
							} else {
301
								$price         = '' === $price ? ''         : $variation->get_price_excluding_tax( 1, $price );
302
								$regular_price = '' === $regular_price ? '' : $variation->get_price_excluding_tax( 1, $regular_price );
303
								$sale_price    = '' === $sale_price ? ''    : $variation->get_price_excluding_tax( 1, $sale_price );
304
							}
305
						}
306
307
						$prices[ $variation_id ]         = wc_format_decimal( $price, wc_get_price_decimals() );
308
						$regular_prices[ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() );
309
						$sale_prices[ $variation_id ]    = wc_format_decimal( $sale_price . '.00', wc_get_price_decimals() );
310
					}
311
				}
312
313
				asort( $prices );
314
				asort( $regular_prices );
315
				asort( $sale_prices );
316
317
				$prices_array[ $price_hash ] = array(
318
					'price'         => $prices,
319
					'regular_price' => $regular_prices,
320
					'sale_price'    => $sale_prices,
321
				);
322
323
				set_transient( $transient_name, json_encode( $prices_array ), DAY_IN_SECONDS * 30 );
324
			}
325
326
			/**
327
			 * Give plugins one last chance to filter the variation prices array which has been generated.
328
			 */
329
			$this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $prices_array[ $price_hash ], $this, $display );
330
		}
331
332
		/**
333
		 * Return the values.
334
		 */
335
		return $this->prices_array[ $price_hash ];
336
	}
337
338
	/**
339
	 * Returns the price in html format.
340
	 *
341
	 * @access public
342
	 * @param string $price (default: '')
343
	 * @return string
344
	 */
345
	public function get_price_html( $price = '' ) {
346
		$prices = $this->get_variation_prices( true );
347
348
		// No variations, or no active variation prices
349
		if ( $this->get_price() === '' || empty( $prices['price'] ) ) {
350
			$price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this );
351
		} else {
352
			$min_price = current( $prices['price'] );
353
			$max_price = end( $prices['price'] );
354
			$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 );
355
			$is_free   = $min_price == 0 && $max_price == 0;
356
357
			if ( $this->is_on_sale() ) {
358
				$min_regular_price = current( $prices['regular_price'] );
359
				$max_regular_price = end( $prices['regular_price'] );
360
				$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 );
361
				$price             = apply_filters( 'woocommerce_variable_sale_price_html', $this->get_price_html_from_to( $regular_price, $price ) . $this->get_price_suffix(), $this );
362
			} elseif ( $is_free ) {
363
				$price = apply_filters( 'woocommerce_variable_free_price_html', __( 'Free!', 'woocommerce' ), $this );
364
			} else {
365
				$price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this );
366
			}
367
		}
368
		return apply_filters( 'woocommerce_get_price_html', $price, $this );
369
	}
370
371
	/**
372
	 * Return an array of attributes used for variations, as well as their possible values.
373
	 *
374
	 * @return array of attributes and their available values
375
	 */
376
	public function get_variation_attributes() {
377
		global $wpdb;
378
379
		$variation_attributes = array();
380
		$attributes           = $this->get_attributes();
381
		$child_ids            = $this->get_children( true );
382
383
		if ( ! empty( $child_ids ) ) {
384
			foreach ( $attributes as $attribute ) {
385
				if ( empty( $attribute['is_variation'] ) ) {
386
					continue;
387
				}
388
389
				// Get possible values for this attribute, for only visible variations.
390
				$values = array_unique( $wpdb->get_col( $wpdb->prepare(
391
					"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN (" . implode( ',', array_map( 'esc_sql', $child_ids ) ) . ")",
392
					wc_variation_attribute_name( $attribute['name'] )
393
				) ) );
394
395
				// empty value indicates that all options for given attribute are available
396
				if ( in_array( '', $values ) ) {
397
					$values = $attribute['is_taxonomy'] ? wp_get_post_terms( $this->id, $attribute['name'], array( 'fields' => 'slugs' ) ) : wc_get_text_attributes( $attribute['value'] );
398
399
				// Get custom attributes (non taxonomy) as defined
400
				} elseif ( ! $attribute['is_taxonomy'] ) {
401
					$text_attributes          = wc_get_text_attributes( $attribute['value'] );
402
					$assigned_text_attributes = $values;
403
					$values                   = array();
404
405
					// Pre 2.4 handling where 'slugs' were saved instead of the full text attribute
406
					if ( version_compare( get_post_meta( $this->id, '_product_version', true ), '2.4.0', '<' ) ) {
407
						$assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes );
408
409
						foreach ( $text_attributes as $text_attribute ) {
410
							if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes ) ) {
411
								$values[] = $text_attribute;
412
							}
413
						}
414
					} else {
415
						foreach ( $text_attributes as $text_attribute ) {
416
							if ( in_array( $text_attribute, $assigned_text_attributes ) ) {
417
								$values[] = $text_attribute;
418
							}
419
						}
420
					}
421
				}
422
423
				$variation_attributes[ $attribute['name'] ] = array_unique( $values );
424
			}
425
		}
426
427
		return $variation_attributes;
428
	}
429
430
	/**
431
	 * If set, get the default attributes for a variable product.
432
	 *
433
	 * @access public
434
	 * @return array
435
	 */
436
	public function get_variation_default_attributes() {
437
		$default = isset( $this->default_attributes ) ? $this->default_attributes : '';
438
		return apply_filters( 'woocommerce_product_default_attributes', array_filter( (array) maybe_unserialize( $default ) ), $this );
439
	}
440
441
	/**
442
	 * Check if variable product has default attributes set.
443
	 *
444
	 * @access public
445
	 * @return bool
446
	 */
447
	public function has_default_attributes() {
448
		if ( ! $this->get_variation_default_attributes() ) {
449
			return true;
450
		}
451
		return false;
452
	}
453
454
	/**
455
	 * If set, get the default attributes for a variable product.
456
	 *
457
	 * @param string $attribute_name
458
	 * @return string
459
	 */
460
	public function get_variation_default_attribute( $attribute_name ) {
461
		$defaults       = $this->get_variation_default_attributes();
462
		$attribute_name = sanitize_title( $attribute_name );
463
		return isset( $defaults[ $attribute_name ] ) ? $defaults[ $attribute_name ] : '';
464
	}
465
466
	/**
467
	 * Match a variation to a given set of attributes using a WP_Query.
468
	 * @since  2.4.0
469
	 * @param  $match_attributes
470
	 * @return int Variation ID which matched, 0 is no match was found
471
	 */
472
	public function get_matching_variation( $match_attributes = array() ) {
473
		global $wpdb;
474
475
		$query_args = array(
476
			'post_parent' => $this->id,
477
			'post_type'   => 'product_variation',
478
			'orderby'     => 'menu_order',
479
			'order'       => 'ASC',
480
			'fields'      => 'ids',
481
			'post_status' => 'publish',
482
			'numberposts' => 1,
483
			'meta_query'  => array()
484
		);
485
486
		foreach ( $this->get_attributes() as $attribute ) {
487
			if ( ! $attribute['is_variation'] ) {
488
				continue;
489
			}
490
491
			$attribute_field_name = 'attribute_' . sanitize_title( $attribute['name'] );
492
493
			if ( ! isset( $match_attributes[ $attribute_field_name ] ) ) {
494
				return 0;
495
			}
496
497
			$value = wc_clean( $match_attributes[ $attribute_field_name ] );
498
499
			$query_args['meta_query'][] = array(
500
				'relation' => 'OR',
501
				array(
502
					'key'     => $attribute_field_name,
503
					'value'   => array( '', $value ),
504
					'compare' => 'IN'
505
				),
506
				array(
507
					'key'     => $attribute_field_name,
508
					'compare' => 'NOT EXISTS'
509
				)
510
			);
511
512
		}
513
514
		// Allow large queries in case user has many variations
515
		$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
516
517
		$matches = get_posts( $query_args );
518
519
		if ( $matches && ! is_wp_error( $matches ) ) {
520
			return current( $matches );
521
522
		/**
523
		 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
524
		 * 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.
525
		 */
526
	 	} elseif ( version_compare( get_post_meta( $this->id, '_product_version', true ), '2.4.0', '<' ) ) {
527
			return $match_attributes === array_map( 'sanitize_title', $match_attributes ) ? 0 : $this->get_matching_variation( array_map( 'sanitize_title', $match_attributes ) );
528
529
		} else {
530
			return 0;
531
		}
532
	}
533
534
	/**
535
	 * Get an array of available variations for the current product.
536
	 * @return array
537
	 */
538
	public function get_available_variations() {
539
		$available_variations = array();
540
541
		foreach ( $this->get_children() as $child_id ) {
542
			$variation = $this->get_child( $child_id );
543
544
			// Hide out of stock variations if 'Hide out of stock items from the catalog' is checked
545
			if ( empty( $variation->variation_id ) || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) {
546
				continue;
547
			}
548
549
			// Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price)
550
			if ( apply_filters( 'woocommerce_hide_invisible_variations', false, $this->id, $variation ) && ! $variation->variation_is_visible() ) {
551
				continue;
552
			}
553
554
			$available_variations[] = $this->get_available_variation( $variation );
555
		}
556
557
		return $available_variations;
558
	}
559
560
	/**
561
	 * Returns an array of data for a variation. Used in the add to cart form.
562
	 * @since  2.4.0
563
	 * @param  WC_Product $variation Variation product object or ID
564
	 * @return array
565
	 */
566
	public function get_available_variation( $variation ) {
567
		if ( is_numeric( $variation ) ) {
568
			$variation = $this->get_child( $variation );
569
		}
570
571
		if ( has_post_thumbnail( $variation->get_variation_id() ) ) {
572
			$attachment_id     = get_post_thumbnail_id( $variation->get_variation_id() );
573
			$attachment        = wp_get_attachment_image_src( $attachment_id, 'shop_single' );
574
			$full_attachment   = wp_get_attachment_image_src( $attachment_id, 'full' );
575
			$attachment_object = get_post( $attachment_id );
576
			$image             = $attachment ? current( $attachment ) : '';
577
			$image_link        = $full_attachment ? current( $full_attachment ) : '';
578
			$image_title       = get_the_title( $attachment_id );
579
			$image_alt         = trim( strip_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
580
			$image_caption     = $attachment_object->post_excerpt;
581
			$image_srcset      = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, 'shop_single' ) : false;
582
			$image_sizes       = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, 'shop_single' ) : false;
583
584
			if ( empty( $image_alt ) ) {
585
				$image_alt = $image_title;
586
			}
587
		} else {
588
			$image = $image_link = $image_title = $image_alt = $image_srcset = $image_sizes = $image_caption = '';
589
		}
590
591
		$availability      = $variation->get_availability();
592
		$availability_html = empty( $availability['availability'] ) ? '' : '<p class="stock ' . esc_attr( $availability['class'] ) . '">' . wp_kses_post( $availability['availability'] ) . '</p>';
593
		$availability_html = apply_filters( 'woocommerce_stock_html', $availability_html, $availability['availability'], $variation );
594
595
		return apply_filters( 'woocommerce_available_variation', array(
596
			'variation_id'           => $variation->variation_id,
597
			'variation_is_visible'   => $variation->variation_is_visible(),
598
			'variation_is_active'    => $variation->variation_is_active(),
599
			'is_purchasable'         => $variation->is_purchasable(),
600
			'display_price'          => $variation->get_display_price(),
601
			'display_regular_price'  => $variation->get_display_price( $variation->get_regular_price() ),
602
			'attributes'             => $variation->get_variation_attributes(),
603
			'image_src'              => $image,
604
			'image_link'             => $image_link,
605
			'image_title'            => $image_title,
606
			'image_alt'              => $image_alt,
607
			'image_caption'          => $image_caption,
608
			'image_srcset'			 => $image_srcset ? $image_srcset : '',
609
			'image_sizes'			 => $image_sizes ? $image_sizes : '',
610
			'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>' : '',
611
			'availability_html'      => $availability_html,
612
			'sku'                    => $variation->get_sku(),
613
			'weight'                 => $variation->get_weight() ? $variation->get_weight() . ' ' . esc_attr( get_option('woocommerce_weight_unit' ) ) : '',
614
			'dimensions'             => $variation->get_dimensions(),
615
			'min_qty'                => 1,
616
			'max_qty'                => $variation->backorders_allowed() ? '' : $variation->get_stock_quantity(),
617
			'backorders_allowed'     => $variation->backorders_allowed(),
618
			'is_in_stock'            => $variation->is_in_stock(),
619
			'is_downloadable'        => $variation->is_downloadable() ,
620
			'is_virtual'             => $variation->is_virtual(),
621
			'is_sold_individually'   => $variation->is_sold_individually() ? 'yes' : 'no',
622
			'variation_description'  => $variation->get_variation_description(),
623
		), $this, $variation );
624
	}
625
626
	/**
627
	 * Sync variable product prices with the children lowest/highest prices.
628
	 */
629
	public function variable_product_sync( $product_id = '' ) {
630
		if ( empty( $product_id ) ) {
631
			$product_id = $this->id;
632
		}
633
634
		// Sync prices with children
635
		self::sync( $product_id );
636
637
		// Re-load prices
638
		$this->price = get_post_meta( $product_id, '_price', true );
639
640
		foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) {
641
			$min_variation_id_key        = "min_{$price_type}_variation_id";
642
			$max_variation_id_key        = "max_{$price_type}_variation_id";
643
			$min_price_key               = "_min_variation_{$price_type}";
644
			$max_price_key               = "_max_variation_{$price_type}";
645
			$this->$min_variation_id_key = get_post_meta( $product_id, '_' . $min_variation_id_key, true );
646
			$this->$max_variation_id_key = get_post_meta( $product_id, '_' . $max_variation_id_key, true );
647
			$this->$min_price_key        = get_post_meta( $product_id, '_' . $min_price_key, true );
648
			$this->$max_price_key        = get_post_meta( $product_id, '_' . $max_price_key, true );
649
		}
650
	}
651
652
	/**
653
	 * Sync variable product stock status with children.
654
	 * @param  int $product_id
655
	 */
656
	public static function sync_stock_status( $product_id ) {
657
		$children = get_posts( array(
658
			'post_parent' 	=> $product_id,
659
			'posts_per_page'=> -1,
660
			'post_type' 	=> 'product_variation',
661
			'fields' 		=> 'ids',
662
			'post_status'	=> 'publish'
663
		) );
664
665
		$stock_status = 'outofstock';
666
667
		foreach ( $children as $child_id ) {
668
			$child_stock_status = get_post_meta( $child_id, '_stock_status', true );
669
			$child_stock_status = $child_stock_status ? $child_stock_status : 'instock';
670
			if ( 'instock' === $child_stock_status ) {
671
				$stock_status = 'instock';
672
				break;
673
			}
674
		}
675
676
		wc_update_product_stock_status( $product_id, $stock_status );
677
	}
678
679
	/**
680
	 * Sync the variable product's attributes with the variations.
681
	 */
682
	public static function sync_attributes( $product_id, $children = false ) {
683
		if ( ! $children ) {
684
			$children = get_posts( array(
685
				'post_parent' 	=> $product_id,
686
				'posts_per_page'=> -1,
687
				'post_type' 	=> 'product_variation',
688
				'fields' 		=> 'ids',
689
				'post_status'	=> 'any'
690
			) );
691
		}
692
693
		/**
694
		 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
695
		 * Attempt to get full version of the text attribute from the parent and UPDATE meta.
696
		 */
697
		if ( version_compare( get_post_meta( $product_id, '_product_version', true ), '2.4.0', '<' ) ) {
698
			$parent_attributes = array_filter( (array) get_post_meta( $product_id, '_product_attributes', true ) );
699
700
			foreach ( $children as $child_id ) {
701
				$all_meta = get_post_meta( $child_id );
702
703
				foreach ( $all_meta as $name => $value ) {
704
					if ( 0 !== strpos( $name, 'attribute_' ) ) {
705
						continue;
706
					}
707
					if ( sanitize_title( $value[0] ) === $value[0] ) {
708 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...
709
							if ( $name !== 'attribute_' . sanitize_title( $attribute['name'] ) ) {
710
								continue;
711
							}
712
							$text_attributes = wc_get_text_attributes( $attribute['value'] );
713
							foreach ( $text_attributes as $text_attribute ) {
714
								if ( sanitize_title( $text_attribute ) === $value[0] ) {
715
									update_post_meta( $child_id, $name, $text_attribute );
716
									break;
717
								}
718
							}
719
						}
720
					}
721
				}
722
			}
723
		}
724
	}
725
726
	/**
727
	 * Does a child have a weight set?
728
	 * @since 2.7.0
729
	 * @return boolean
730
	 */
731
	public function child_has_weight() {
732
		return (bool) get_post_meta( $this->id, '_child_has_weight', true );
733
	}
734
735
	/**
736
	 * Does a child have dimensions set?
737
	 * @since 2.7.0
738
	 * @return boolean
739
	 */
740
	public function child_has_dimensions() {
741
		return (bool) get_post_meta( $this->id, '_child_has_dimensions', true );
742
	}
743
744
	/**
745
	 * Returns whether or not we are showing dimensions on the product page.
746
	 *
747
	 * @return bool
748
	 */
749
	public function enable_dimensions_display() {
750
		return apply_filters( 'wc_product_enable_dimensions_display', true ) && ( $this->has_dimensions() || $this->has_weight() || $this->child_has_weight() || $this->child_has_dimensions() );
751
	}
752
753
	/**
754
	 * Sync the variable product with it's children.
755
	 */
756
	public static function sync( $product_id ) {
757
		global $wpdb;
758
759
		$children = get_posts( array(
760
			'post_parent' 	=> $product_id,
761
			'posts_per_page'=> -1,
762
			'post_type' 	=> 'product_variation',
763
			'fields' 		=> 'ids',
764
			'post_status'	=> 'publish'
765
		) );
766
767
		// No published variations - product won't be purchasable.
768
		if ( ! $children ) {
769
			update_post_meta( $product_id, '_price', '' );
770
			delete_post_meta( $product_id, '_child_has_weight' );
771
			delete_post_meta( $product_id, '_child_has_dimensions' );
772
			delete_transient( 'wc_products_onsale' );
773
774
			if ( is_admin() && 'publish' === get_post_status( $product_id ) ) {
775
				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' ) );
776
			}
777
778
		// Loop the variations
779
		} else {
780
781
			// Set the variable product to be virtual/downloadable if all children are virtual/downloadable
782
			foreach ( array( '_downloadable', '_virtual' ) as $meta_key ) {
783
				$all_variations_yes = true;
784
785
				foreach ( $children as $child_id ) {
786
					if ( 'yes' != get_post_meta( $child_id, $meta_key, true ) ) {
787
						$all_variations_yes = false;
788
						break;
789
					}
790
				}
791
792
				update_post_meta( $product_id, $meta_key, ( true === $all_variations_yes ) ? 'yes' : 'no' );
793
			}
794
795
			// Main active prices
796
			$min_price            = null;
797
			$max_price            = null;
798
			$min_price_id         = null;
799
			$max_price_id         = null;
800
801
			// Regular prices
802
			$min_regular_price    = null;
803
			$max_regular_price    = null;
804
			$min_regular_price_id = null;
805
			$max_regular_price_id = null;
806
807
			// Sale prices
808
			$min_sale_price       = null;
809
			$max_sale_price       = null;
810
			$min_sale_price_id    = null;
811
			$max_sale_price_id    = null;
812
813
			foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) {
814
				foreach ( $children as $child_id ) {
815
					$child_price = get_post_meta( $child_id, '_' . $price_type, true );
816
817
					// Skip non-priced variations
818
					if ( $child_price === '' ) {
819
						continue;
820
					}
821
822
					// Skip hidden variations
823
					if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
824
						$stock = get_post_meta( $child_id, '_stock', true );
825
						if ( $stock !== "" && $stock <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
826
							continue;
827
						}
828
					}
829
830
					// Find min price
831
					if ( is_null( ${"min_{$price_type}"} ) || $child_price < ${"min_{$price_type}"} ) {
832
						${"min_{$price_type}"}    = $child_price;
833
						${"min_{$price_type}_id"} = $child_id;
834
					}
835
836
					// Find max price
837
					if ( $child_price > ${"max_{$price_type}"} ) {
838
						${"max_{$price_type}"}    = $child_price;
839
						${"max_{$price_type}_id"} = $child_id;
840
					}
841
				}
842
843
				// Store prices
844
				update_post_meta( $product_id, '_min_variation_' . $price_type, ${"min_{$price_type}"} );
845
				update_post_meta( $product_id, '_max_variation_' . $price_type, ${"max_{$price_type}"} );
846
847
				// Store ids
848
				update_post_meta( $product_id, '_min_' . $price_type . '_variation_id', ${"min_{$price_type}_id"} );
849
				update_post_meta( $product_id, '_max_' . $price_type . '_variation_id', ${"max_{$price_type}_id"} );
850
			}
851
852
			// Sync _price meta
853
			delete_post_meta( $product_id, '_price' );
854
			add_post_meta( $product_id, '_price', $min_price, false );
855
			add_post_meta( $product_id, '_price', $max_price, false );
856
			delete_transient( 'wc_products_onsale' );
857
858
			// Sync weights
859
			foreach ( $children as $child_id ) {
860
				if ( get_post_meta( $child_id, '_weight', true ) ) {
861
					update_post_meta( $product_id, '_child_has_weight', true );
862
					break;
863
				}
864
			}
865
866
			// Sync dimensions
867
			foreach ( $children as $child_id ) {
868
				if ( get_post_meta( $child_id, '_height', true ) || get_post_meta( $child_id, '_width', true ) || get_post_meta( $child_id, '_length', true ) ) {
869
					update_post_meta( $product_id, '_child_has_dimensions', true );
870
					break;
871
				}
872
			}
873
874
			// Sync attributes
875
			self::sync_attributes( $product_id, $children );
876
877
			do_action( 'woocommerce_variable_product_sync', $product_id, $children );
878
		}
879
	}
880
}
881