Completed
Pull Request — master (#11971)
by Aristeides
07:37
created

WC_Product_Variable::get_matching_variation()   C

Complexity

Conditions 8
Paths 13

Size

Total Lines 65
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 35
nc 13
nop 1
dl 0
loc 65
rs 6.7651
c 0
b 0
f 0

How to fix   Long Method   

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
		$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 ( ! empty( $this->prices_array[ $price_hash ] ) ) {
261
			/**
262
			 * $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.
263
			 * If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered.
264
			 */
265
			return $this->prices_array[ $price_hash ];
266
267
		} else {
268
			/**
269
			 * No locally cached value? Get the data from the transient or generate it.
270
			 */
271
272
			// Get value of transient
273
			$transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) );
274
275
			// If the product version has changed since the transient was last saved, reset the transient cache.
276
			if ( empty( $transient_cached_prices_array['version'] ) || WC_Cache_Helper::get_transient_version( 'product' ) !== $transient_cached_prices_array['version'] ) {
277
				$transient_cached_prices_array = array( 'version' => WC_Cache_Helper::get_transient_version( 'product' ) );
278
			}
279
280
			// If the prices are not stored for this hash, generate them and add to the transient.
281
			if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) {
282
				$prices         = array();
283
				$regular_prices = array();
284
				$sale_prices    = array();
285
				$variation_ids  = $this->get_children( true );
286
287
				foreach ( $variation_ids as $variation_id ) {
288
					if ( $variation = $this->get_child( $variation_id ) ) {
289
						$price         = apply_filters( 'woocommerce_variation_prices_price', $variation->price, $variation, $this );
290
						$regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->regular_price, $variation, $this );
291
						$sale_price    = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->sale_price, $variation, $this );
292
293
						// Skip empty prices
294
						if ( '' === $price ) {
295
							continue;
296
						}
297
298
						// If sale price does not equal price, the product is not yet on sale
299
						if ( $sale_price === $regular_price || $sale_price !== $price ) {
300
							$sale_price = $regular_price;
301
						}
302
303
						// If we are getting prices for display, we need to account for taxes
304
						if ( $display ) {
305
							if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
306
								$price         = '' === $price ? ''         : $variation->get_price_including_tax( 1, $price );
307
								$regular_price = '' === $regular_price ? '' : $variation->get_price_including_tax( 1, $regular_price );
308
								$sale_price    = '' === $sale_price ? ''    : $variation->get_price_including_tax( 1, $sale_price );
309
							} else {
310
								$price         = '' === $price ? ''         : $variation->get_price_excluding_tax( 1, $price );
311
								$regular_price = '' === $regular_price ? '' : $variation->get_price_excluding_tax( 1, $regular_price );
312
								$sale_price    = '' === $sale_price ? ''    : $variation->get_price_excluding_tax( 1, $sale_price );
313
							}
314
						}
315
316
						$prices[ $variation_id ]         = wc_format_decimal( $price, wc_get_price_decimals() );
317
						$regular_prices[ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() );
318
						$sale_prices[ $variation_id ]    = wc_format_decimal( $sale_price . '.00', wc_get_price_decimals() );
319
					}
320
				}
321
322
				asort( $prices );
323
				asort( $regular_prices );
324
				asort( $sale_prices );
325
326
				$transient_cached_prices_array[ $price_hash ] = array(
327
					'price'         => $prices,
328
					'regular_price' => $regular_prices,
329
					'sale_price'    => $sale_prices,
330
				);
331
332
				set_transient( $transient_name, json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 );
333
			}
334
335
			/**
336
			 * Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class.
337
			 * This value may differ from the transient cache. It is filtered once before storing locally.
338
			 */
339
			return $this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $this, $display );
340
		}
341
	}
342
343
	/**
344
	 * Returns the price in html format.
345
	 *
346
	 * @access public
347
	 * @param string $price (default: '')
348
	 * @return string
349
	 */
350
	public function get_price_html( $price = '' ) {
351
		$prices = $this->get_variation_prices( true );
352
353
		// No variations, or no active variation prices
354
		if ( $this->get_price() === '' || empty( $prices['price'] ) ) {
355
			$price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this );
356
		} else {
357
			$min_price = current( $prices['price'] );
358
			$max_price = end( $prices['price'] );
359
			$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 );
360
			$is_free   = 0 == $min_price && 0 == $max_price;
361
362
			if ( $this->is_on_sale() ) {
363
				$min_regular_price = current( $prices['regular_price'] );
364
				$max_regular_price = end( $prices['regular_price'] );
365
				$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 );
366
				$price             = apply_filters( 'woocommerce_variable_sale_price_html', $this->get_price_html_from_to( $regular_price, $price ) . $this->get_price_suffix(), $this );
367
			} elseif ( $is_free ) {
368
				$price = apply_filters( 'woocommerce_variable_free_price_html', __( 'Free!', 'woocommerce' ), $this );
369
			} else {
370
				$price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this );
371
			}
372
		}
373
		return apply_filters( 'woocommerce_get_price_html', $price, $this );
374
	}
375
376
	/**
377
	 * Return an array of attributes used for variations, as well as their possible values.
378
	 *
379
	 * @return array of attributes and their available values
380
	 */
381
	public function get_variation_attributes() {
382
		global $wpdb;
383
384
		$variation_attributes = array();
385
		$attributes           = $this->get_attributes();
386
		$child_ids            = $this->get_children( true );
387
388
		if ( ! empty( $child_ids ) ) {
389
			foreach ( $attributes as $attribute ) {
390
				if ( empty( $attribute['is_variation'] ) ) {
391
					continue;
392
				}
393
394
				// Get possible values for this attribute, for only visible variations.
395
				$values = array_unique( $wpdb->get_col( $wpdb->prepare(
396
					"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN (" . implode( ',', array_map( 'esc_sql', $child_ids ) ) . ")",
397
					wc_variation_attribute_name( $attribute['name'] )
398
				) ) );
399
400
				// empty value indicates that all options for given attribute are available
401
				if ( in_array( '', $values ) ) {
402
					$values = $attribute['is_taxonomy'] ? wp_get_post_terms( $this->id, $attribute['name'], array( 'fields' => 'slugs' ) ) : wc_get_text_attributes( $attribute['value'] );
403
				} elseif ( ! $attribute['is_taxonomy'] ) {
404
					// Get custom attributes (non taxonomy) as defined.
405
					$text_attributes          = wc_get_text_attributes( $attribute['value'] );
406
					$assigned_text_attributes = $values;
407
					$values                   = array();
408
409
					// Pre 2.4 handling where 'slugs' were saved instead of the full text attribute
410
					if ( version_compare( get_post_meta( $this->id, '_product_version', true ), '2.4.0', '<' ) ) {
411
						$assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes );
412
413
						foreach ( $text_attributes as $text_attribute ) {
414
							if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes ) ) {
415
								$values[] = $text_attribute;
416
							}
417
						}
418
					} else {
419
						foreach ( $text_attributes as $text_attribute ) {
420
							if ( in_array( $text_attribute, $assigned_text_attributes ) ) {
421
								$values[] = $text_attribute;
422
							}
423
						}
424
					}
425
				}
426
427
				$variation_attributes[ $attribute['name'] ] = array_unique( $values );
428
			}
429
		}
430
431
		return $variation_attributes;
432
	}
433
434
	/**
435
	 * If set, get the default attributes for a variable product.
436
	 *
437
	 * @access public
438
	 * @return array
439
	 */
440
	public function get_variation_default_attributes() {
441
		$default = isset( $this->default_attributes ) ? $this->default_attributes : '';
442
		return apply_filters( 'woocommerce_product_default_attributes', array_filter( (array) maybe_unserialize( $default ) ), $this );
443
	}
444
445
	/**
446
	 * Check if variable product has default attributes set.
447
	 *
448
	 * @access public
449
	 * @return bool
450
	 */
451
	public function has_default_attributes() {
452
		if ( ! $this->get_variation_default_attributes() ) {
453
			return true;
454
		}
455
		return false;
456
	}
457
458
	/**
459
	 * If set, get the default attributes for a variable product.
460
	 *
461
	 * @param string $attribute_name
462
	 * @return string
463
	 */
464
	public function get_variation_default_attribute( $attribute_name ) {
465
		$defaults       = $this->get_variation_default_attributes();
466
		$attribute_name = sanitize_title( $attribute_name );
467
		return isset( $defaults[ $attribute_name ] ) ? $defaults[ $attribute_name ] : '';
468
	}
469
470
	/**
471
	 * Match a variation to a given set of attributes using a WP_Query.
472
	 * @since  2.4.0
473
	 * @param  $match_attributes
474
	 * @return int Variation ID which matched, 0 is no match was found
475
	 */
476
	public function get_matching_variation( $match_attributes = array() ) {
477
		global $wpdb;
478
479
		$query_args = array(
480
			'post_parent' => $this->id,
481
			'post_type'   => 'product_variation',
482
			'orderby'     => 'menu_order',
483
			'order'       => 'ASC',
484
			'fields'      => 'ids',
485
			'post_status' => 'publish',
486
			'numberposts' => 1,
487
			'meta_query'  => array(),
488
		);
489
490
		foreach ( $this->get_attributes() as $attribute ) {
491
			if ( ! $attribute['is_variation'] ) {
492
				continue;
493
			}
494
495
			$attribute_field_name = 'attribute_' . sanitize_title( $attribute['name'] );
496
497
			if ( ! isset( $match_attributes[ $attribute_field_name ] ) ) {
498
				return 0;
499
			}
500
501
			$value = wc_clean( $match_attributes[ $attribute_field_name ] );
502
503
			$query_args['meta_query'][] = array(
504
				'relation' => 'OR',
505
				array(
506
					'key'     => $attribute_field_name,
507
					'value'   => array( '', $value ),
508
					'compare' => 'IN',
509
				),
510
				array(
511
					'key'     => $attribute_field_name,
512
					'compare' => 'NOT EXISTS',
513
				)
514
			);
515
516
		}
517
518
		// Allow large queries in case user has many variations
519
		$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
520
521
		$matches = get_posts( $query_args );
522
523
		if ( $matches && ! is_wp_error( $matches ) ) {
524
525
			return current( $matches );
526
527
	 	} elseif ( version_compare( get_post_meta( $this->id, '_product_version', true ), '2.4.0', '<' ) ) {
528
529
			/**
530
			 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
531
			 * 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.
532
			 */
533
			return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->get_matching_variation( array_map( 'sanitize_title', $match_attributes ) );
534
535
		} else {
536
537
			return 0;
538
539
		}
540
	}
541
542
	/**
543
	 * Get an array of available variations for the current product.
544
	 * @return array
545
	 */
546
	public function get_available_variations() {
547
		$available_variations = array();
548
549
		foreach ( $this->get_children() as $child_id ) {
550
			$variation = $this->get_child( $child_id );
551
552
			// Hide out of stock variations if 'Hide out of stock items from the catalog' is checked
553
			if ( empty( $variation->variation_id ) || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) {
554
				continue;
555
			}
556
557
			// Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price)
558
			if ( apply_filters( 'woocommerce_hide_invisible_variations', false, $this->id, $variation ) && ! $variation->variation_is_visible() ) {
559
				continue;
560
			}
561
562
			$available_variations[] = $this->get_available_variation( $variation );
563
		}
564
565
		return $available_variations;
566
	}
567
568
	/**
569
	 * Returns an array of data for a variation. Used in the add to cart form.
570
	 * @since  2.4.0
571
	 * @param  WC_Product $variation Variation product object or ID
572
	 * @return array
573
	 */
574
	public function get_available_variation( $variation ) {
575
		if ( is_numeric( $variation ) ) {
576
			$variation = $this->get_child( $variation );
577
		}
578
579
		if ( has_post_thumbnail( $variation->get_variation_id() ) ) {
580
			$attachment_id     = get_post_thumbnail_id( $variation->get_variation_id() );
581
			$attachment        = wp_get_attachment_image_src( $attachment_id, 'shop_single' );
582
			$full_attachment   = wp_get_attachment_image_src( $attachment_id, 'full' );
583
			$attachment_object = get_post( $attachment_id );
584
			$image             = $attachment ? current( $attachment ) : '';
585
			$image_link        = $full_attachment ? current( $full_attachment ) : '';
586
			$image_title       = get_the_title( $attachment_id );
587
			$image_alt         = trim( strip_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
588
			$image_caption     = $attachment_object->post_excerpt;
589
			$image_srcset      = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, 'shop_single' ) : false;
590
			$image_sizes       = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, 'shop_single' ) : false;
591
592
			if ( empty( $image_alt ) ) {
593
				$image_alt = $image_title;
594
			}
595
		} else {
596
			$image = $image_link = $image_title = $image_alt = $image_srcset = $image_sizes = $image_caption = '';
597
		}
598
599
		$availability      = $variation->get_availability();
600
		$availability_html = empty( $availability['availability'] ) ? '' : '<p class="stock ' . esc_attr( $availability['class'] ) . '">' . wp_kses_post( $availability['availability'] ) . '</p>';
601
		$availability_html = apply_filters( 'woocommerce_stock_html', $availability_html, $availability['availability'], $variation );
602
603
		return apply_filters( 'woocommerce_available_variation', array(
604
			'variation_id'           => $variation->variation_id,
605
			'variation_is_visible'   => $variation->variation_is_visible(),
606
			'variation_is_active'    => $variation->variation_is_active(),
607
			'is_purchasable'         => $variation->is_purchasable(),
608
			'display_price'          => $variation->get_display_price(),
609
			'display_regular_price'  => $variation->get_display_price( $variation->get_regular_price() ),
610
			'attributes'             => $variation->get_variation_attributes(),
611
			'image_src'              => $image,
612
			'image_link'             => $image_link,
613
			'image_title'            => $image_title,
614
			'image_alt'              => $image_alt,
615
			'image_caption'          => $image_caption,
616
			'image_srcset'			 => $image_srcset ? $image_srcset : '',
617
			'image_sizes'			 => $image_sizes ? $image_sizes : '',
618
			'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>' : '',
619
			'availability_html'      => $availability_html,
620
			'sku'                    => $variation->get_sku(),
621
			'weight'                 => $variation->get_weight() ? $variation->get_weight() . ' ' . esc_attr( get_option( 'woocommerce_weight_unit' ) ) : '',
622
			'dimensions'             => $variation->get_dimensions(),
623
			'min_qty'                => 1,
624
			'max_qty'                => $variation->backorders_allowed() ? '' : $variation->get_stock_quantity(),
625
			'backorders_allowed'     => $variation->backorders_allowed(),
626
			'is_in_stock'            => $variation->is_in_stock(),
627
			'is_downloadable'        => $variation->is_downloadable(),
628
			'is_virtual'             => $variation->is_virtual(),
629
			'is_sold_individually'   => $variation->is_sold_individually() ? 'yes' : 'no',
630
			'variation_description'  => $variation->get_variation_description(),
631
		), $this, $variation );
632
	}
633
634
	/**
635
	 * Sync variable product prices with the children lowest/highest prices.
636
	 */
637
	public function variable_product_sync( $product_id = '' ) {
638
		if ( empty( $product_id ) ) {
639
			$product_id = $this->id;
640
		}
641
642
		// Sync prices with children
643
		self::sync( $product_id );
644
645
		// Re-load prices
646
		$this->price = get_post_meta( $product_id, '_price', true );
647
648
		foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) {
649
			$min_variation_id_key        = "min_{$price_type}_variation_id";
650
			$max_variation_id_key        = "max_{$price_type}_variation_id";
651
			$min_price_key               = "_min_variation_{$price_type}";
652
			$max_price_key               = "_max_variation_{$price_type}";
653
			$this->$min_variation_id_key = get_post_meta( $product_id, '_' . $min_variation_id_key, true );
654
			$this->$max_variation_id_key = get_post_meta( $product_id, '_' . $max_variation_id_key, true );
655
			$this->$min_price_key        = get_post_meta( $product_id, '_' . $min_price_key, true );
656
			$this->$max_price_key        = get_post_meta( $product_id, '_' . $max_price_key, true );
657
		}
658
	}
659
660
	/**
661
	 * Sync variable product stock status with children.
662
	 * @param  int $product_id
663
	 */
664
	public static function sync_stock_status( $product_id ) {
665
		$children = get_posts( array(
666
			'post_parent' 	=> $product_id,
667
			'posts_per_page' => -1,
668
			'post_type' 	=> 'product_variation',
669
			'fields' 		=> 'ids',
670
			'post_status'	=> 'publish',
671
		) );
672
673
		$stock_status = 'outofstock';
674
675
		foreach ( $children as $child_id ) {
676
			$child_stock_status = get_post_meta( $child_id, '_stock_status', true );
677
			$child_stock_status = $child_stock_status ? $child_stock_status : 'instock';
678
			if ( 'instock' === $child_stock_status ) {
679
				$stock_status = 'instock';
680
				break;
681
			}
682
		}
683
684
		wc_update_product_stock_status( $product_id, $stock_status );
685
	}
686
687
	/**
688
	 * Sync the variable product's attributes with the variations.
689
	 */
690
	public static function sync_attributes( $product_id, $children = false ) {
691
		if ( ! $children ) {
692
			$children = get_posts( array(
693
				'post_parent' 	=> $product_id,
694
				'posts_per_page' => -1,
695
				'post_type' 	=> 'product_variation',
696
				'fields' 		=> 'ids',
697
				'post_status'	=> 'any',
698
			) );
699
		}
700
701
		/**
702
		 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
703
		 * Attempt to get full version of the text attribute from the parent and UPDATE meta.
704
		 */
705
		if ( version_compare( get_post_meta( $product_id, '_product_version', true ), '2.4.0', '<' ) ) {
706
			$parent_attributes = array_filter( (array) get_post_meta( $product_id, '_product_attributes', true ) );
707
708
			foreach ( $children as $child_id ) {
709
				$all_meta = get_post_meta( $child_id );
710
711
				foreach ( $all_meta as $name => $value ) {
712
					if ( 0 !== strpos( $name, 'attribute_' ) ) {
713
						continue;
714
					}
715
					if ( sanitize_title( $value[0] ) === $value[0] ) {
716 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...
717
							if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) {
718
								continue;
719
							}
720
							$text_attributes = wc_get_text_attributes( $attribute['value'] );
721
							foreach ( $text_attributes as $text_attribute ) {
722
								if ( sanitize_title( $text_attribute ) === $value[0] ) {
723
									update_post_meta( $child_id, $name, $text_attribute );
724
									break;
725
								}
726
							}
727
						}
728
					}
729
				}
730
			}
731
		}
732
	}
733
734
	/**
735
	 * Does a child have a weight set?
736
	 * @since 2.7.0
737
	 * @return boolean
738
	 */
739
	public function child_has_weight() {
740
		return (bool) get_post_meta( $this->id, '_child_has_weight', true );
741
	}
742
743
	/**
744
	 * Does a child have dimensions set?
745
	 * @since 2.7.0
746
	 * @return boolean
747
	 */
748
	public function child_has_dimensions() {
749
		return (bool) get_post_meta( $this->id, '_child_has_dimensions', true );
750
	}
751
752
	/**
753
	 * Returns whether or not we are showing dimensions on the product page.
754
	 *
755
	 * @return bool
756
	 */
757
	public function enable_dimensions_display() {
758
		return apply_filters( 'wc_product_enable_dimensions_display', true ) && ( $this->has_dimensions() || $this->has_weight() || $this->child_has_weight() || $this->child_has_dimensions() );
759
	}
760
761
	/**
762
	 * Sync the variable product with it's children.
763
	 */
764
	public static function sync( $product_id ) {
765
		global $wpdb;
766
767
		$children = get_posts( array(
768
			'post_parent' 	=> $product_id,
769
			'posts_per_page' => -1,
770
			'post_type' 	=> 'product_variation',
771
			'fields' 		=> 'ids',
772
			'post_status'	=> 'publish',
773
		) );
774
775
		// No published variations - product won't be purchasable.
776
		if ( ! $children ) {
777
			update_post_meta( $product_id, '_price', '' );
778
			delete_post_meta( $product_id, '_child_has_weight' );
779
			delete_post_meta( $product_id, '_child_has_dimensions' );
780
			delete_transient( 'wc_products_onsale' );
781
782
			if ( is_admin() && 'publish' === get_post_status( $product_id ) ) {
783
				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' ) );
784
			}
785
		} else { // Loop the variations.
786
787
			// Set the variable product to be virtual/downloadable if all children are virtual/downloadable
788
			foreach ( array( '_downloadable', '_virtual' ) as $meta_key ) {
789
				$all_variations_yes = true;
790
791
				foreach ( $children as $child_id ) {
792
					if ( 'yes' != get_post_meta( $child_id, $meta_key, true ) ) {
793
						$all_variations_yes = false;
794
						break;
795
					}
796
				}
797
798
				update_post_meta( $product_id, $meta_key, ( true === $all_variations_yes ) ? 'yes' : 'no' );
799
			}
800
801
			// Main active prices
802
			$min_price            = null;
803
			$max_price            = null;
804
			$min_price_id         = null;
805
			$max_price_id         = null;
806
807
			// Regular prices
808
			$min_regular_price    = null;
809
			$max_regular_price    = null;
810
			$min_regular_price_id = null;
811
			$max_regular_price_id = null;
812
813
			// Sale prices
814
			$min_sale_price       = null;
815
			$max_sale_price       = null;
816
			$min_sale_price_id    = null;
817
			$max_sale_price_id    = null;
818
819
			foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) {
820
				foreach ( $children as $child_id ) {
821
					$child_price = get_post_meta( $child_id, '_' . $price_type, true );
822
823
					// Skip non-priced variations
824
					if ( '' === $child_price ) {
825
						continue;
826
					}
827
828
					// Skip hidden variations
829
					if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
830
						$stock = get_post_meta( $child_id, '_stock', true );
831
						if ( '' !== $stock && $stock <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
832
							continue;
833
						}
834
					}
835
836
					// Find min price
837
					if ( is_null( ${"min_{$price_type}"} ) || $child_price < ${"min_{$price_type}"} ) {
838
						${"min_{$price_type}"}    = $child_price;
839
						${"min_{$price_type}_id"} = $child_id;
840
					}
841
842
					// Find max price
843
					if ( $child_price > ${"max_{$price_type}"} ) {
844
						${"max_{$price_type}"}    = $child_price;
845
						${"max_{$price_type}_id"} = $child_id;
846
					}
847
				}
848
849
				// Store prices
850
				update_post_meta( $product_id, '_min_variation_' . $price_type, ${"min_{$price_type}"} );
851
				update_post_meta( $product_id, '_max_variation_' . $price_type, ${"max_{$price_type}"} );
852
853
				// Store ids
854
				update_post_meta( $product_id, '_min_' . $price_type . '_variation_id', ${"min_{$price_type}_id"} );
855
				update_post_meta( $product_id, '_max_' . $price_type . '_variation_id', ${"max_{$price_type}_id"} );
856
			}
857
858
			// Sync _price meta
859
			delete_post_meta( $product_id, '_price' );
860
			add_post_meta( $product_id, '_price', $min_price, false );
861
			add_post_meta( $product_id, '_price', $max_price, false );
862
			delete_transient( 'wc_products_onsale' );
863
864
			// Sync weights
865
			foreach ( $children as $child_id ) {
866
				if ( get_post_meta( $child_id, '_weight', true ) ) {
867
					update_post_meta( $product_id, '_child_has_weight', true );
868
					break;
869
				}
870
			}
871
872
			// Sync dimensions
873
			foreach ( $children as $child_id ) {
874
				if ( get_post_meta( $child_id, '_height', true ) || get_post_meta( $child_id, '_width', true ) || get_post_meta( $child_id, '_length', true ) ) {
875
					update_post_meta( $product_id, '_child_has_dimensions', true );
876
					break;
877
				}
878
			}
879
880
			// Sync attributes
881
			self::sync_attributes( $product_id, $children );
882
883
			do_action( 'woocommerce_variable_product_sync', $product_id, $children );
884
		}
885
	}
886
}
887