1 <?php
  2 
  3 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
  4 
  5 /**
  6  * Product Variation Class
  7  *
  8  * The WooCommerce product variation class handles product variation data.
  9  *
 10  * @class       WC_Product_Variation
 11  * @version     2.1.0
 12  * @package     WooCommerce/Classes
 13  * @category    Class
 14  * @author      WooThemes
 15  */
 16 class WC_Product_Variation extends WC_Product {
 17 
 18     /** @public int ID of the variable product. */
 19     public $variation_id;
 20 
 21     /** @public object Parent Variable product object. */
 22     public $parent;
 23 
 24     /** @public array Stores variation data (attributes) for the current variation. */
 25     public $variation_data = array();
 26 
 27     /** @public bool True if the variation has a length. */
 28     public $variation_has_length = false;
 29 
 30     /** @public bool True if the variation has a width. */
 31     public $variation_has_width = false;
 32 
 33     /** @public bool True if the variation has a height. */
 34     public $variation_has_height = false;
 35 
 36     /** @public bool True if the variation has a weight. */
 37     public $variation_has_weight = false;
 38 
 39     /** @public bool True if the variation has stock and is managing stock. */
 40     public $variation_has_stock = false;
 41 
 42     /** @public bool True if the variation has a sku. */
 43     public $variation_has_sku = false;
 44 
 45     /** @public string Stores the shipping class of the variation. */
 46     public $variation_shipping_class = false;
 47 
 48     /** @public int Stores the shipping class ID of the variation. */
 49     public $variation_shipping_class_id = false;
 50 
 51     /** @public bool True if the variation has a tax class. */
 52     public $variation_has_tax_class = false;
 53 
 54     /** @public bool True if the variation has file paths. */
 55     public $variation_has_downloadable_files = false;
 56 
 57     /**
 58      * Loads all product data from custom fields
 59      *
 60      * @access public
 61      * @param int $variation_id ID of the variation to load
 62      * @param array $args Array of the arguments containing parent product data
 63      * @return void
 64      */
 65     public function __construct( $variation, $args = array() ) {
 66 
 67         $this->product_type = 'variation';
 68 
 69         if ( is_object( $variation ) ) {
 70             $this->variation_id = absint( $variation->ID );
 71         } else {
 72             $this->variation_id = absint( $variation );
 73         }
 74 
 75         /* Get main product data from parent (args) */
 76         $this->id   = ! empty( $args['parent_id'] ) ? intval( $args['parent_id'] ) : wp_get_post_parent_id( $this->variation_id );
 77 
 78         // The post doesn't have a parent id, therefore its invalid.
 79         if ( empty( $this->id ) )
 80             return;
 81 
 82         // Get post data
 83         $this->parent = ! empty( $args['parent'] ) ? $args['parent'] : get_product( $this->id );
 84         $this->post   = ! empty( $this->parent->post ) ? $this->parent->post : array();
 85         $this->product_custom_fields = get_post_meta( $this->variation_id );
 86 
 87         // Get the variation attributes from meta
 88         foreach ( $this->product_custom_fields as $name => $value ) {
 89             if ( ! strstr( $name, 'attribute_' ) )
 90                 continue;
 91 
 92             $this->variation_data[ $name ] = sanitize_title( $value[0] );
 93         }
 94 
 95         // Now get variation meta to override the parent variable product
 96         if ( ! empty( $this->product_custom_fields['_sku'][0] ) ) {
 97             $this->variation_has_sku = true;
 98             $this->sku               = $this->product_custom_fields['_sku'][0];
 99         }
100 
101         if ( ! empty( $this->product_custom_fields['_downloadable_files'][0] ) ) {
102             $this->variation_has_downloadable_files = true;
103             $this->downloadable_files               = $this->product_custom_fields['_downloadable_files'][0];
104         }
105 
106         if ( isset( $this->product_custom_fields['_stock'][0] ) && '' !== $this->product_custom_fields['_stock'][0] && 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
107             $this->variation_has_stock = true;
108             $this->manage_stock        = 'yes';
109             $this->stock               = $this->product_custom_fields['_stock'][0];
110         }
111 
112         if ( isset( $this->product_custom_fields['_weight'][0] ) && $this->product_custom_fields['_weight'][0] !== '' ) {
113             $this->variation_has_weight = true;
114             $this->weight               = $this->product_custom_fields['_weight'][0];
115         }
116 
117         if ( isset( $this->product_custom_fields['_length'][0] ) && $this->product_custom_fields['_length'][0] !== '' ) {
118             $this->variation_has_length = true;
119             $this->length               = $this->product_custom_fields['_length'][0];
120         }
121 
122         if ( isset( $this->product_custom_fields['_width'][0] ) && $this->product_custom_fields['_width'][0] !== '' ) {
123             $this->variation_has_width = true;
124             $this->width               = $this->product_custom_fields['_width'][0];
125         }
126 
127         if ( isset( $this->product_custom_fields['_height'][0] ) && $this->product_custom_fields['_height'][0] !== '' ) {
128             $this->variation_has_height = true;
129             $this->height               = $this->product_custom_fields['_height'][0];
130         }
131 
132         if ( isset( $this->product_custom_fields['_downloadable'][0] ) && $this->product_custom_fields['_downloadable'][0] == 'yes' ) {
133             $this->downloadable = 'yes';
134         } else {
135             $this->downloadable = 'no';
136         }
137 
138         if ( isset( $this->product_custom_fields['_virtual'][0] ) && $this->product_custom_fields['_virtual'][0] == 'yes' ) {
139             $this->virtual = 'yes';
140         } else {
141             $this->virtual = 'no';
142         }
143 
144         if ( isset( $this->product_custom_fields['_tax_class'][0] ) ) {
145             $this->variation_has_tax_class = true;
146             $this->tax_class               = $this->product_custom_fields['_tax_class'][0];
147         }
148 
149         if ( isset( $this->product_custom_fields['_sale_price_dates_from'][0] ) )
150             $this->sale_price_dates_from = $this->product_custom_fields['_sale_price_dates_from'][0];
151 
152         if ( isset( $this->product_custom_fields['_sale_price_dates_to'][0] ) )
153             $this->sale_price_dates_to = $this->product_custom_fields['_sale_price_dates_to'][0];
154 
155         // Prices
156         $this->price         = isset( $this->product_custom_fields['_price'][0] ) ? $this->product_custom_fields['_price'][0] : '';
157         $this->regular_price = isset( $this->product_custom_fields['_regular_price'][0] ) ? $this->product_custom_fields['_regular_price'][0] : '';
158         $this->sale_price    = isset( $this->product_custom_fields['_sale_price'][0] ) ? $this->product_custom_fields['_sale_price'][0] : '';
159 
160         // Backwards compat for prices
161         if ( $this->price !== '' && $this->regular_price == '' ) {
162             update_post_meta( $this->variation_id, '_regular_price', $this->price );
163             $this->regular_price = $this->price;
164 
165             if ( $this->sale_price !== '' && $this->sale_price < $this->regular_price ) {
166                 update_post_meta( $this->variation_id, '_price', $this->sale_price );
167                 $this->price = $this->sale_price;
168             }
169         }
170 
171         $this->total_stock = $this->stock;
172     }
173 
174     /**
175      * Returns whether or not the product post exists.
176      *
177      * @access public
178      * @return bool
179      */
180     public function exists() {
181         return empty( $this->id ) ? false : true;
182     }
183 
184     /**
185      * Wrapper for get_permalink. Adds this variations attributes to the URL.
186      * @return string
187      */
188     public function get_permalink() {
189         return add_query_arg( $this->variation_data, get_permalink( $this->id ) );
190     }
191 
192     /**
193      * Get the add to url used mainly in loops.
194      *
195      * @access public
196      * @return string
197      */
198     public function add_to_cart_url() {
199         $url = $this->is_purchasable() && $this->is_in_stock() ? remove_query_arg( 'added-to-cart', add_query_arg( array_merge( array( 'variation_id' => $this->variation_id, 'add-to-cart' => $this->id ), $this->variation_data ) ) ) : get_permalink( $this->id );
200 
201         return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this );
202     }
203 
204     /**
205      * Get the add to cart button text
206      *
207      * @access public
208      * @return string
209      */
210     public function add_to_cart_text() {
211         $text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Add to cart', 'woocommerce' ) : __( 'Read More', 'woocommerce' );
212 
213         return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this );
214     }
215 
216     /**
217      * Checks if this particular variation is visible (variations with no price, or out of stock, can be hidden)
218      *
219      * @return bool
220      */
221     public function variation_is_visible() {
222         $visible = true;
223 
224         // Published == enabled checkbox
225         if ( get_post_status( $this->variation_id ) != 'publish' )
226             $visible = false;
227 
228         // Out of stock visibility
229         elseif ( get_option('woocommerce_hide_out_of_stock_items') == 'yes' && ! $this->is_in_stock() )
230             $visible = false;
231 
232         // Price not set
233         elseif ( $this->get_price() === "" )
234             $visible = false;
235 
236         return apply_filters( 'woocommerce_variation_is_visible', $visible, $this->variation_id, $this->id );
237     }
238 
239     /**
240      * Returns false if the product cannot be bought.
241      *
242      * @access public
243      * @return bool
244      */
245     public function is_purchasable() {
246 
247         // Published == enabled checkbox
248         if ( get_post_status( $this->variation_id ) != 'publish' )
249             $purchasable = false;
250 
251         else
252             $purchasable = parent::is_purchasable();
253 
254         return $purchasable;
255     }
256 
257     /**
258      * Returns whether or not the variations parent is visible.
259      *
260      * @access public
261      * @return bool
262      */
263     public function parent_is_visible() {
264         return $this->is_visible();
265     }
266 
267     /**
268      * Get variation ID
269      *
270      * @return int
271      */
272     public function get_variation_id() {
273         return absint( $this->variation_id );
274     }
275 
276     /**
277      * Get variation attribute values
278      *
279      * @return array of attributes and their values for this variation
280      */
281     public function get_variation_attributes() {
282         return $this->variation_data;
283     }
284 
285     /**
286      * Get variation price HTML. Prices are not inherited from parents.
287      *
288      * @return string containing the formatted price
289      */
290     public function get_price_html( $price = '' ) {
291 
292         $tax_display_mode      = get_option( 'woocommerce_tax_display_shop' );
293         $display_price         = $tax_display_mode == 'incl' ? $this->get_price_including_tax() : $this->get_price_excluding_tax();
294         $display_regular_price = $tax_display_mode == 'incl' ? $this->get_price_including_tax( 1, $this->get_regular_price() ) : $this->get_price_excluding_tax( 1, $this->get_regular_price() );
295         $display_sale_price    = $tax_display_mode == 'incl' ? $this->get_price_including_tax( 1, $this->get_sale_price() ) : $this->get_price_excluding_tax( 1, $this->get_sale_price() );
296 
297         if ( $this->get_price() !== '' ) {
298             if ( $this->is_on_sale() ) {
299 
300                 $price = '<del>' . wc_price( $display_regular_price ) . '</del> <ins>' . wc_price( $display_sale_price ) . '</ins>' . $this->get_price_suffix();
301 
302                 $price = apply_filters( 'woocommerce_variation_sale_price_html', $price, $this );
303 
304             } elseif ( $this->get_price() > 0 ) {
305 
306                 $price = wc_price( $display_price ) . $this->get_price_suffix();
307 
308                 $price = apply_filters( 'woocommerce_variation_price_html', $price, $this );
309 
310             } else {
311 
312                 $price = __( 'Free!', 'woocommerce' );
313 
314                 $price = apply_filters( 'woocommerce_variation_free_price_html', $price, $this );
315 
316             }
317         } else {
318             $price = apply_filters( 'woocommerce_variation_empty_price_html', '', $this );
319         }
320 
321         return apply_filters( 'woocommerce_get_variation_price_html', $price, $this );
322     }
323 
324     /**
325      * Gets the main product image ID.
326      * @return int
327      */
328     public function get_image_id() {
329         if ( $this->variation_id && has_post_thumbnail( $this->variation_id ) ) {
330             $image_id = get_post_thumbnail_id( $this->variation_id );
331         } elseif ( has_post_thumbnail( $this->id ) ) {
332             $image_id = get_post_thumbnail_id( $this->id );
333         } elseif ( ( $parent_id = wp_get_post_parent_id( $this->id ) ) && has_post_thumbnail( $parent_id ) ) {
334             $image_id = get_post_thumbnail_id( $parent_id );
335         } else {
336             $image_id = 0;
337         }
338         return $image_id;
339     }
340 
341     /**
342      * Gets the main product image.
343      *
344      * @access public
345      * @param string $size (default: 'shop_thumbnail')
346      * @return string
347      */
348     public function get_image( $size = 'shop_thumbnail', $attr = array() ) {
349         if ( $this->variation_id && has_post_thumbnail( $this->variation_id ) ) {
350             $image = get_the_post_thumbnail( $this->variation_id, $size, $attr );
351         } elseif ( has_post_thumbnail( $this->id ) ) {
352             $image = get_the_post_thumbnail( $this->id, $size, $attr );
353         } elseif ( ( $parent_id = wp_get_post_parent_id( $this->id ) ) && has_post_thumbnail( $parent_id ) ) {
354             $image = get_the_post_thumbnail( $parent_id, $size , $attr);
355         } else {
356             $image = wc_placeholder_img( $size );
357         }
358 
359         return $image;
360     }
361 
362     /**
363      * Set stock level of the product variation.
364      * @param int  $amount
365      * @param bool $force_variation_stock If true, the variation's stock will be updated and not the parents.
366      * @return int
367      * @todo Need to return 0 if is_null? Or something. Should not be just return.
368      */
369     function set_stock( $amount = null, $force_variation_stock = false ) {
370         if ( is_null( $amount ) )
371             return;
372 
373         if ( $amount === '' && $force_variation_stock ) {
374 
375             // If amount is an empty string, stock management is being turned off at variation level
376             $this->variation_has_stock = false;
377             $this->stock               = '';
378             unset( $this->manage_stock );
379 
380             // Update meta
381             update_post_meta( $this->variation_id, '_stock', '' );
382 
383             // Refresh parent prices
384             WC_Product_Variable::sync( $this->id );
385 
386         } elseif ( $this->variation_has_stock || $force_variation_stock ) {
387 
388             // Update stock amount
389             $this->stock               = intval( $amount );
390             $this->variation_has_stock = true;
391             $this->manage_stock        = 'yes';
392 
393             // Update meta
394             update_post_meta( $this->variation_id, '_stock', $this->stock );
395 
396             // Clear total stock transient
397             delete_transient( 'wc_product_total_stock_' . $this->id );
398 
399             // Check parents out of stock attribute
400             if ( ! $this->is_in_stock() ) {
401 
402                 // Check parent
403                 $parent_product = get_product( $this->id );
404 
405                 // Only continue if the parent has backorders off and all children are stock managed and out of stock
406                 if ( ! $parent_product->backorders_allowed() && $parent_product->get_total_stock() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
407 
408                     $all_managed = true;
409 
410                     if ( sizeof( $parent_product->get_children() ) > 0 ) {
411                         foreach ( $parent_product->get_children() as $child_id ) {
412                             $stock = get_post_meta( $child_id, '_stock', true );
413                             if ( $stock == '' ) {
414                                 $all_managed = false;
415                                 break;
416                             }
417                         }
418                     }
419 
420                     if ( $all_managed ) {
421                         $this->set_stock_status( 'outofstock' );
422                     }
423                 }
424 
425             } elseif ( $this->is_in_stock() ) {
426                 $this->set_stock_status( 'instock' );
427             }
428 
429             // Refresh parent prices
430             WC_Product_Variable::sync( $this->id );
431 
432             // Trigger action
433             do_action( 'woocommerce_product_set_stock', $this );
434 
435             return $this->get_stock_quantity();
436 
437         } else {
438 
439             return parent::set_stock( $amount );
440 
441         }
442     }
443 
444     /**
445      * Reduce stock level of the product.
446      *
447      * @param int $by (default: 1) Amount to reduce by
448      * @return int stock level
449      */
450     public function reduce_stock( $by = 1 ) {
451         if ( $this->variation_has_stock ) {
452             return $this->set_stock( $this->stock - $by );
453         } else {
454             return parent::reduce_stock( $by );
455         }
456     }
457 
458     /**
459      * Increase stock level of the product.
460      *
461      * @param int $by (default: 1) Amount to increase by
462      * @return int stock level
463      */
464     public function increase_stock( $by = 1 ) {
465         if ( $this->variation_has_stock ) {
466             return $this->set_stock( $this->stock + $by );
467         } else {
468             return parent::increase_stock( $by );
469         }
470     }
471 
472     /**
473      * Get the shipping class, and if not set, get the shipping class of the parent.
474      *
475      * @access public
476      * @return string
477      */
478     public function get_shipping_class() {
479         if ( ! $this->variation_shipping_class ) {
480             $classes = get_the_terms( $this->variation_id, 'product_shipping_class' );
481 
482             if ( $classes && ! is_wp_error( $classes ) ) {
483                 $this->variation_shipping_class = esc_attr( current( $classes )->slug );
484             } else {
485                 $this->variation_shipping_class = parent::get_shipping_class();
486             }
487         }
488 
489         return $this->variation_shipping_class;
490     }
491 
492     /**
493      * Returns the product shipping class ID.
494      *
495      * @access public
496      * @return int
497      */
498     public function get_shipping_class_id() {
499         if ( ! $this->variation_shipping_class_id ) {
500 
501             $classes = get_the_terms( $this->variation_id, 'product_shipping_class' );
502 
503             if ( $classes && ! is_wp_error( $classes ) )
504                 $this->variation_shipping_class_id = current( $classes )->term_id;
505             else
506                 $this->variation_shipping_class_id = parent::get_shipping_class_id();
507 
508         }
509         return absint( $this->variation_shipping_class_id );
510     }
511 
512     /**
513      * Get product name with extra details such as SKU, price and attributes. Used within admin.
514      *
515      * @access public
516      * @param mixed $product
517      * @return string Formatted product name, including attributes and price
518      */
519     public function get_formatted_name() {
520 
521         if ( $this->get_sku() )
522             $identifier = $this->get_sku();
523         else
524             $identifier = '#' . $this->variation_id;
525 
526         $attributes = $this->get_variation_attributes();
527         $extra_data = ' &ndash; ' . implode( ', ', $attributes ) . ' &ndash; ' . wc_price( $this->get_price() );
528 
529         return sprintf( __( '%s &ndash; %s%s', 'woocommerce' ), $identifier, $this->get_title(), $extra_data );
530     }
531 }
532 
WooCommerce API documentation generated by ApiGen 2.8.0