1 <?php
  2 
  3 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
  4 
  5 /**
  6  * Variable Product Class
  7  *
  8  * The WooCommerce product class handles individual product data.
  9  *
 10  * @class       WC_Product_Variable
 11  * @version     2.0.0
 12  * @package     WooCommerce/Classes/Products
 13  * @category    Class
 14  * @author      WooThemes
 15  */
 16 class WC_Product_Variable extends WC_Product {
 17 
 18     /** @public array Array of child products/posts/variations. */
 19     public $children;
 20 
 21     /** @public string The product's total stock, including that of its children. */
 22     public $total_stock;
 23 
 24     /**
 25      * __construct function.
 26      *
 27      * @access public
 28      * @param mixed $product
 29      */
 30     public function __construct( $product ) {
 31         $this->product_type = 'variable';
 32         parent::__construct( $product );
 33     }
 34 
 35     /**
 36      * Get the add to cart button text
 37      *
 38      * @access public
 39      * @return string
 40      */
 41     public function add_to_cart_text() {
 42         return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Select options', 'woocommerce' ), $this );
 43     }
 44 
 45     /**
 46      * Get total stock.
 47      *
 48      * This is the stock of parent and children combined.
 49      *
 50      * @access public
 51      * @return int
 52      */
 53     public function get_total_stock() {
 54 
 55         if ( empty( $this->total_stock ) ) {
 56 
 57             $transient_name = 'wc_product_total_stock_' . $this->id;
 58 
 59             if ( false === ( $this->total_stock = get_transient( $transient_name ) ) ) {
 60                 $this->total_stock = $this->stock;
 61 
 62                 if ( sizeof( $this->get_children() ) > 0 ) {
 63                     foreach ($this->get_children() as $child_id) {
 64                         $stock = get_post_meta( $child_id, '_stock', true );
 65 
 66                         if ( $stock != '' ) {
 67                             $this->total_stock += intval( $stock );
 68                         }
 69                     }
 70                 }
 71 
 72                 set_transient( $transient_name, $this->total_stock, YEAR_IN_SECONDS );
 73             }
 74         }
 75         return apply_filters( 'woocommerce_stock_amount', $this->total_stock );
 76     }
 77 
 78     /**
 79      * Set stock level of the product.
 80      *
 81      * @param mixed $amount (default: null)
 82      * @param string $mode can be set, add, or subtract
 83      * @return int Stock
 84      */
 85     function set_stock( $amount = null, $mode = 'set' ) {
 86         // Empty total stock so its refreshed
 87         $this->total_stock = '';
 88 
 89         // Call parent set_stock
 90         return parent::set_stock( $amount, $mode );
 91     }
 92 
 93     /**
 94      * Return the products children posts.
 95      *
 96      * @param  boolean $visible_only Only return variations which are not hidden
 97      * @return array of children ids
 98      */
 99     public function get_children( $visible_only = false ) {
100 
101         if ( ! is_array( $this->children ) ) {
102             $this->children = array();
103 
104             $transient_name = 'wc_product_children_ids_' . $this->id;
105 
106             if ( false === ( $this->children = get_transient( $transient_name ) ) ) {
107                 $this->children = get_posts( 'post_parent=' . $this->id . '&post_type=product_variation&orderby=menu_order&order=ASC&fields=ids&post_status=any&numberposts=-1' );
108 
109                 set_transient( $transient_name, $this->children, YEAR_IN_SECONDS );
110             }
111         }
112 
113         if ( $visible_only ) {
114             $children = array();
115             foreach ( $this->children as $child_id ) {
116                 if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
117                     $stock = get_post_meta( $child_id, '_stock', true );
118                     if ( $stock !== "" && $stock <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
119                         continue;
120                     }
121                 }
122                 $children[] = $child_id;
123             }
124         } else {
125             $children = $this->children;
126         }
127 
128         return $children;
129     }
130 
131 
132     /**
133      * get_child function.
134      *
135      * @access public
136      * @param mixed $child_id
137      * @return object WC_Product or WC_Product_variation
138      */
139     public function get_child( $child_id ) {
140         return get_product( $child_id, array(
141             'parent_id' => $this->id,
142             'parent'    => $this
143             ) );
144     }
145 
146 
147     /**
148      * Returns whether or not the product has any child product.
149      *
150      * @access public
151      * @return bool
152      */
153     public function has_child() {
154         return sizeof( $this->get_children() ) ? true : false;
155     }
156 
157 
158     /**
159      * Returns whether or not the product is on sale.
160      *
161      * @access public
162      * @return bool
163      */
164     public function is_on_sale() {
165         if ( $this->has_child() ) {
166 
167             foreach ( $this->get_children( true ) as $child_id ) {
168                 $price      = get_post_meta( $child_id, '_price', true );
169                 $sale_price = get_post_meta( $child_id, '_sale_price', true );
170                 if ( $sale_price !== "" && $sale_price >= 0 && $sale_price == $price )
171                     return true;
172             }
173 
174         }
175         return false;
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         $variation_id = get_post_meta( $this->id, '_' . $min_or_max . '_regular_price_variation_id', true );
186 
187         if ( ! $variation_id ) {
188             return false;
189         }
190 
191         $price        = get_post_meta( $variation_id, '_regular_price', true );
192 
193         if ( $display ) {
194             $variation        = $this->get_child( $variation_id );
195             $tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
196             $price            = $tax_display_mode == 'incl' ? $variation->get_price_including_tax( 1, $price ) : $variation->get_price_excluding_tax( 1, $price );
197         }
198 
199         return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $display );
200     }
201 
202     /**
203      * Get the min or max variation sale 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_sale_price( $min_or_max = 'min', $display = false ) {
209         $variation_id = get_post_meta( $this->id, '_' . $min_or_max . '_sale_price_variation_id', true );
210 
211         if ( ! $variation_id ) {
212             return false;
213         }
214 
215         $price        = get_post_meta( $variation_id, '_sale_price', true );
216 
217         if ( $display ) {
218             $variation        = $this->get_child( $variation_id );
219             $tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
220             $price            = $tax_display_mode == 'incl' ? $variation->get_price_including_tax( 1, $price ) : $variation->get_price_excluding_tax( 1, $price );
221         }
222 
223         return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $display );
224     }
225 
226     /**
227      * Get the min or max variation (active) price.
228      * @param  string $min_or_max - min or max
229      * @param  boolean  $display Whether the value is going to be displayed
230      * @return string
231      */
232     public function get_variation_price( $min_or_max = 'min', $display = false ) {
233         $variation_id = get_post_meta( $this->id, '_' . $min_or_max . '_price_variation_id', true );
234 
235         if ( $display ) {
236             $variation        = $this->get_child( $variation_id );
237 
238             if ( $variation ) {
239                 $tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
240                 $price            = $tax_display_mode == 'incl' ? $variation->get_price_including_tax() : $variation->get_price_excluding_tax();
241             } else {
242                 $price = '';
243             }
244         } else {
245             $price = get_post_meta( $variation_id, '_price', true );
246         }
247 
248         return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $display );
249     }
250 
251     /**
252      * Returns the price in html format.
253      *
254      * @access public
255      * @param string $price (default: '')
256      * @return string
257      */
258     public function get_price_html( $price = '' ) {
259 
260         // Ensure variation prices are synced with variations
261         if ( $this->get_variation_regular_price( 'min' ) === false || $this->get_variation_price( 'min' ) === false || $this->get_variation_price( 'min' ) === '' || $this->get_price() === '' ) {
262             $this->variable_product_sync( $this->id );
263         }
264 
265         // Get the price
266         if ( $this->get_price() === '' ) {
267 
268             $price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this );
269 
270         } else {
271 
272             // Main price
273             $prices = array( $this->get_variation_price( 'min', true ), $this->get_variation_price( 'max', true ) );
274             $price = $prices[0] !== $prices[1] ? sprintf( _x( '%1$s&ndash;%2$s', 'Price range: from-to', 'woocommerce' ), wc_price( $prices[0] ), wc_price( $prices[1] ) ) : wc_price( $prices[0] );
275 
276             // Sale
277             $prices = array( $this->get_variation_regular_price( 'min', true ), $this->get_variation_regular_price( 'max', true ) );
278             sort( $prices );
279             $saleprice = $prices[0] !== $prices[1] ? sprintf( _x( '%1$s&ndash;%2$s', 'Price range: from-to', 'woocommerce' ), wc_price( $prices[0] ), wc_price( $prices[1] ) ) : wc_price( $prices[0] );
280 
281             if ( $price !== $saleprice ) {
282                 $price = apply_filters( 'woocommerce_variable_sale_price_html', $this->get_price_html_from_to( $saleprice, $price ) . $this->get_price_suffix(), $this );
283             } else {
284                 $price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this );
285             }
286 
287         }
288 
289         return apply_filters( 'woocommerce_get_price_html', $price, $this );
290     }
291 
292 
293     /**
294      * Return an array of attributes used for variations, as well as their possible values.
295      *
296      * @access public
297      * @return array of attributes and their available values
298      */
299     public function get_variation_attributes() {
300 
301         $variation_attributes = array();
302 
303         if ( ! $this->has_child() )
304             return $variation_attributes;
305 
306         $attributes = $this->get_attributes();
307 
308         foreach ( $attributes as $attribute ) {
309             if ( ! $attribute['is_variation'] )
310                 continue;
311 
312             $values = array();
313             $attribute_field_name = 'attribute_' . sanitize_title( $attribute['name'] );
314 
315             foreach ( $this->get_children() as $child_id ) {
316 
317                 $variation = $this->get_child( $child_id );
318 
319                 if ( ! empty( $variation->variation_id ) ) {
320 
321                     if ( ! $variation->variation_is_visible() )
322                         continue; // Disabled or hidden
323 
324                     $child_variation_attributes = $variation->get_variation_attributes();
325 
326                     foreach ( $child_variation_attributes as $name => $value )
327                         if ( $name == $attribute_field_name )
328                             $values[] = sanitize_title( $value );
329                 }
330             }
331 
332             // empty value indicates that all options for given attribute are available
333             if ( in_array( '', $values ) ) {
334 
335                 $values = array();
336 
337                 // Get all options
338                 if ( $attribute['is_taxonomy'] ) {
339                     $post_terms = wp_get_post_terms( $this->id, $attribute['name'] );
340                     foreach ( $post_terms as $term )
341                         $values[] = $term->slug;
342                 } else {
343                     $values = array_map( 'trim', explode( WC_DELIMITER, $attribute['value'] ) );
344                 }
345 
346                 $values = array_unique( $values );
347 
348             // Order custom attributes (non taxonomy) as defined
349             } elseif ( ! $attribute['is_taxonomy'] ) {
350 
351                 $option_names = array_map( 'trim', explode( WC_DELIMITER, $attribute['value'] ) );
352                 $option_slugs = $values;
353                 $values       = array();
354 
355                 foreach ( $option_names as $option_name ) {
356                     if ( in_array( sanitize_title( $option_name ), $option_slugs ) )
357                         $values[] = $option_name;
358                 }
359             }
360 
361             $variation_attributes[ $attribute['name'] ] = array_unique( $values );
362         }
363 
364         return $variation_attributes;
365     }
366 
367     /**
368      * If set, get the default attributes for a variable product.
369      *
370      * @access public
371      * @return array
372      */
373     public function get_variation_default_attributes() {
374 
375         $default = isset( $this->default_attributes ) ? $this->default_attributes : '';
376 
377         return apply_filters( 'woocommerce_product_default_attributes', (array) maybe_unserialize( $default ), $this );
378     }
379 
380     /**
381      * Get an array of available variations for the current product.
382      *
383      * @access public
384      * @return array
385      */
386     public function get_available_variations() {
387 
388         $available_variations = array();
389 
390         foreach ( $this->get_children() as $child_id ) {
391 
392             $variation = $this->get_child( $child_id );
393 
394             if ( ! empty( $variation->variation_id ) ) {
395                 $variation_attributes   = $variation->get_variation_attributes();
396                 $availability           = $variation->get_availability();
397                 $availability_html      = empty( $availability['availability'] ) ? '' : apply_filters( 'woocommerce_stock_html', '<p class="stock ' . esc_attr( $availability['class'] ) . '">'. wp_kses_post( $availability['availability'] ).'</p>', wp_kses_post( $availability['availability'] ) );
398 
399                 if ( has_post_thumbnail( $variation->get_variation_id() ) ) {
400                     $attachment_id = get_post_thumbnail_id( $variation->get_variation_id() );
401 
402                     $attachment    = wp_get_attachment_image_src( $attachment_id, apply_filters( 'single_product_large_thumbnail_size', 'shop_single' )  );
403                     $image         = $attachment ? current( $attachment ) : '';
404 
405                     $attachment    = wp_get_attachment_image_src( $attachment_id, 'full'  );
406                     $image_link    = $attachment ? current( $attachment ) : '';
407 
408                     $image_title   = get_the_title( $attachment_id );
409                     $image_alt     = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
410                 } else {
411                     $image = $image_link = $image_title = $image_alt = '';
412                 }
413 
414                 $available_variations[] = apply_filters( 'woocommerce_available_variation', array(
415                     'variation_id'         => $child_id,
416                     'variation_is_visible' => $variation->variation_is_visible(),
417                     'is_purchasable'       => $variation->is_purchasable(),
418                     'attributes'           => $variation_attributes,
419                     'image_src'            => $image,
420                     'image_link'           => $image_link,
421                     'image_title'          => $image_title,
422                     'image_alt'            => $image_alt,
423                     'price_html'           => $variation->get_price() === "" || $this->get_variation_price( 'min' ) !== $this->get_variation_price( 'max' ) ? '<span class="price">' . $variation->get_price_html() . '</span>' : '',
424                     'availability_html'    => $availability_html,
425                     'sku'                  => $variation->get_sku(),
426                     'weight'               => $variation->get_weight() . ' ' . esc_attr( get_option('woocommerce_weight_unit' ) ),
427                     'dimensions'           => $variation->get_dimensions(),
428                     'min_qty'              => 1,
429                     'max_qty'              => $this->backorders_allowed() ? '' : $variation->stock,
430                     'backorders_allowed'   => $this->backorders_allowed(),
431                     'is_in_stock'          => $variation->is_in_stock(),
432                     'is_downloadable'      => $variation->is_downloadable() ,
433                     'is_virtual'           => $variation->is_virtual(),
434                     'is_sold_individually' => $variation->is_sold_individually() ? 'yes' : 'no',
435                 ), $this, $variation );
436             }
437         }
438 
439         return $available_variations;
440     }
441 
442     /**
443      * Sync variable product prices with the children lowest/highest prices.
444      */
445     public function variable_product_sync( $product_id = '' ) {
446         if ( empty( $product_id ) )
447             $product_id = $this->id;
448 
449         // Sync prices with children
450         self::sync( $product_id );
451 
452         // Re-load prices
453         $this->price                  = get_post_meta( $product_id, '_price', true );
454 
455         foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) {
456             $min_variation_id_key        = "min_{$price_type}_variation_id";
457             $max_variation_id_key        = "max_{$price_type}_variation_id";
458             $min_price_key               = "_min_variation_{$price_type}";
459             $max_price_key               = "_max_variation_{$price_type}";
460             $this->$min_variation_id_key = get_post_meta( $product_id, '_' . $min_variation_id_key, true );
461             $this->$max_variation_id_key = get_post_meta( $product_id, '_' . $max_variation_id_key, true );
462             $this->$min_price_key        = get_post_meta( $product_id, '_' . $min_price_key, true );
463             $this->$max_price_key        = get_post_meta( $product_id, '_' . $max_price_key, true );
464         }
465     }
466 
467     /**
468      * Sync the variable product with it's children
469      */
470     public static function sync( $product_id ) {
471         global $wpdb;
472 
473         $children = get_posts( array(
474             'post_parent'   => $product_id,
475             'posts_per_page'=> -1,
476             'post_type'     => 'product_variation',
477             'fields'        => 'ids',
478             'post_status'   => 'publish'
479         ) );
480 
481         // No published variations - update parent post status. Use $wpdb to prevent endless loop on save_post hooks.
482         if ( ! $children && get_post_status( $product_id ) == 'publish' ) {
483             $wpdb->update( $wpdb->posts, array( 'post_status' => 'draft' ), array( 'ID' => $product_id ) );
484 
485             if ( is_admin() ) {
486                 WC_Admin_Meta_Boxes::add_error( __( 'This variable product has no active variations so cannot be published. Changing status to draft.', 'woocommerce' ) );
487             }
488 
489         // Loop the variations
490         } else {
491             // Main active prices
492             $min_price            = null;
493             $max_price            = null;
494             $min_price_id         = null;
495             $max_price_id         = null;
496 
497             // Regular prices
498             $min_regular_price    = null;
499             $max_regular_price    = null;
500             $min_regular_price_id = null;
501             $max_regular_price_id = null;
502 
503             // Sale prices
504             $min_sale_price       = null;
505             $max_sale_price       = null;
506             $min_sale_price_id    = null;
507             $max_sale_price_id    = null;
508 
509             foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) {
510                 foreach ( $children as $child_id ) {
511                     $child_price = get_post_meta( $child_id, '_' . $price_type, true );
512 
513                     // Skip non-priced variations
514                     if ( $child_price === '' ) {
515                         continue;
516                     }
517 
518                     // Skip hidden variations
519                     if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
520                         $stock = get_post_meta( $child_id, '_stock', true );
521                         if ( $stock !== "" && $stock <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
522                             continue;
523                         }
524                     }
525 
526                     // Find min price
527                     if ( is_null( ${"min_{$price_type}"} ) || $child_price < ${"min_{$price_type}"} ) {
528                         ${"min_{$price_type}"}    = $child_price;
529                         ${"min_{$price_type}_id"} = $child_id;
530                     }
531 
532                     // Find max price
533                     if ( $child_price > ${"max_{$price_type}"} ) {
534                         ${"max_{$price_type}"}    = $child_price;
535                         ${"max_{$price_type}_id"} = $child_id;
536                     }
537                 }
538 
539                 // Store prices
540                 update_post_meta( $product_id, '_min_variation_' . $price_type, ${"min_{$price_type}"} );
541                 update_post_meta( $product_id, '_max_variation_' . $price_type, ${"max_{$price_type}"} );
542 
543                 // Store ids
544                 update_post_meta( $product_id, '_min_' . $price_type . '_variation_id', ${"min_{$price_type}_id"} );
545                 update_post_meta( $product_id, '_max_' . $price_type . '_variation_id', ${"max_{$price_type}_id"} );
546             }
547 
548             // The VARIABLE PRODUCT price should equal the min price of any type
549             update_post_meta( $product_id, '_price', $min_price );
550 
551             do_action( 'woocommerce_variable_product_sync', $product_id, $children );
552 
553             wc_delete_product_transients( $product_id );
554         }
555     }
556 }
557 
WooCommerce API documentation generated by ApiGen 2.8.0