1 <?php
   2 /**
   3  * Abstract Product Class
   4  *
   5  * The WooCommerce product class handles individual product data.
   6  *
   7  * @class       WC_Product
   8  * @version     2.1.0
   9  * @package     WooCommerce/Abstracts
  10  * @category    Abstract Class
  11  * @author      WooThemes
  12  */
  13 class WC_Product {
  14 
  15     /** @var int The product (post) ID. */
  16     public $id;
  17 
  18     /** @var object The actual post object. */
  19     public $post;
  20 
  21     /** @var string The product's type (simple, variable etc). */
  22     public $product_type = null;
  23 
  24     /**
  25      * Constructor gets the post object and sets the ID for the loaded product.
  26      *
  27      * @access public
  28      * @param int|WC_Product|WP_Post $product Product ID, post object, or product object
  29      */
  30     public function __construct( $product ) {
  31         if ( is_numeric( $product ) ) {
  32             $this->id   = absint( $product );
  33             $this->post = get_post( $this->id );
  34         } elseif ( $product instanceof WC_Product ) {
  35             $this->id   = absint( $product->id );
  36             $this->post = $product;
  37         } elseif ( $product instanceof WP_Post || isset( $product->ID ) ) {
  38             $this->id   = absint( $product->ID );
  39             $this->post = $product;
  40         }
  41     }
  42 
  43     /**
  44      * __isset function.
  45      *
  46      * @access public
  47      * @param mixed $key
  48      * @return bool
  49      */
  50     public function __isset( $key ) {
  51         return metadata_exists( 'post', $this->id, '_' . $key );
  52     }
  53 
  54     /**
  55      * __get function.
  56      *
  57      * @access public
  58      * @param mixed $key
  59      * @return mixed
  60      */
  61     public function __get( $key ) {
  62 
  63         // Get values or default if not set
  64         if ( in_array( $key, array( 'downloadable', 'virtual', 'backorders', 'manage_stock', 'featured', 'sold_individually' ) ) ) {
  65             $value = ( $value = get_post_meta( $this->id, '_' . $key, true ) ) ? $value : 'no';
  66 
  67         } elseif ( in_array( $key, array( 'product_attributes', 'crosssell_ids', 'upsell_ids' ) ) ) {
  68             $value = ( $value = get_post_meta( $this->id, '_' . $key, true ) ) ? $value : array();
  69 
  70         } elseif ( 'visibility' == $key ) {
  71             $value = ( $value = get_post_meta( $this->id, '_visibility', true ) ) ? $value : 'hidden';
  72 
  73         } elseif ( 'stock' == $key ) {
  74             $value = ( $value = get_post_meta( $this->id, '_stock', true ) ) ? $value : 0;
  75 
  76         } elseif ( 'stock_status' == $key ) {
  77             $value = ( $value = get_post_meta( $this->id, '_stock_status', true ) ) ? $value : 'instock';
  78 
  79         } elseif ( 'tax_status' == $key ) {
  80             $value = ( $value = get_post_meta( $this->id, '_tax_status', true ) ) ? $value : 'taxable';
  81 
  82         } else {
  83             $value = get_post_meta( $this->id, '_' . $key, true );
  84         }
  85 
  86         return $value;
  87     }
  88 
  89     /**
  90      * Get the product's post data.
  91      *
  92      * @access public
  93      * @return object
  94      */
  95     public function get_post_data() {
  96         return $this->post;
  97     }
  98 
  99     /**
 100      * get_gallery_attachment_ids function.
 101      *
 102      * @access public
 103      * @return array
 104      */
 105     public function get_gallery_attachment_ids() {
 106         if ( ! isset( $this->product_image_gallery ) ) {
 107             // Backwards compat
 108             $attachment_ids = get_posts( 'post_parent=' . $this->id . '&numberposts=-1&post_type=attachment&orderby=menu_order&order=ASC&post_mime_type=image&fields=ids&meta_key=_woocommerce_exclude_image&meta_value=0' );
 109             $attachment_ids = array_diff( $attachment_ids, array( get_post_thumbnail_id() ) );
 110             $this->product_image_gallery = implode( ',', $attachment_ids );
 111         }
 112         return apply_filters( 'woocommerce_product_gallery_attachment_ids', array_filter( (array) explode( ',', $this->product_image_gallery ) ), $this );
 113     }
 114 
 115     /**
 116      * Wrapper for get_permalink
 117      * @return string
 118      */
 119     public function get_permalink() {
 120         return get_permalink( $this->id );
 121     }
 122 
 123     /**
 124      * Get SKU (Stock-keeping unit) - product unique ID.
 125      *
 126      * @return string
 127      */
 128     public function get_sku() {
 129         return $this->sku;
 130     }
 131 
 132     /**
 133      * Returns number of items available for sale.
 134      *
 135      * @access public
 136      * @return int
 137      */
 138     public function get_stock_quantity() {
 139         return $this->managing_stock() ? apply_filters( 'woocommerce_stock_amount', $this->stock ) : '';
 140     }
 141 
 142     /**
 143      * Get total stock.
 144      *
 145      * @access public
 146      * @return int
 147      */
 148     public function get_total_stock() {
 149         return $this->get_stock_quantity();
 150     }
 151 
 152     /**
 153      * Set stock level of the product.
 154      *
 155      * @param mixed $amount (default: null)
 156      * @return int Stock
 157      */
 158     public function set_stock( $amount = null ) {
 159         if ( is_null( $amount ) ) {
 160             return 0;
 161         }
 162 
 163         if ( $this->managing_stock() ) {
 164 
 165             // Update stock amount
 166             $this->stock = apply_filters( 'woocommerce_stock_amount', $amount );
 167 
 168             // Update meta
 169             update_post_meta( $this->id, '_stock', $this->stock );
 170 
 171             // Update stock status
 172             if ( ! $this->backorders_allowed() && $this->get_total_stock() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
 173                 $this->set_stock_status( 'outofstock' );
 174 
 175             } elseif ( $this->backorders_allowed() || $this->get_total_stock() > get_option( 'woocommerce_notify_no_stock_amount' ) ) {
 176                 $this->set_stock_status( 'instock' );
 177             }
 178 
 179             // Clear total stock transient
 180             delete_transient( 'wc_product_total_stock_' . $this->id );
 181 
 182             // Trigger action
 183             do_action( 'woocommerce_product_set_stock', $this );
 184 
 185             return $this->get_stock_quantity();
 186         }
 187 
 188         return 0;
 189     }
 190 
 191     /**
 192      * Reduce stock level of the product.
 193      *
 194      * @param int $by (default: 1) Amount to reduce by.
 195      * @return int Stock
 196      */
 197     public function reduce_stock( $by = 1 ) {
 198         return $this->set_stock( $this->stock - $by );
 199     }
 200 
 201     /**
 202      * Increase stock level of the product.
 203      *
 204      * @param int $by (default: 1) Amount to increase by
 205      * @return int Stock
 206      */
 207     public function increase_stock( $by = 1 ) {
 208         return $this->set_stock( $this->stock + $by );
 209     }
 210 
 211     /**
 212      * set_stock_status function.
 213      *
 214      * @access public
 215      * @return void
 216      */
 217     public function set_stock_status( $status ) {
 218         $status = ( 'outofstock' === $status ) ? 'outofstock' : 'instock';
 219 
 220         if ( update_post_meta( $this->id, '_stock_status', $status ) ) {
 221             do_action( 'woocommerce_product_set_stock_status', $this->id, $status );
 222         }
 223     }
 224 
 225     /**
 226      * Checks the product type.
 227      *
 228      * Backwards compat with downloadable/virtual.
 229      *
 230      * @access public
 231      * @param mixed $type Array or string of types
 232      * @return bool
 233      */
 234     public function is_type( $type ) {
 235         return ( $this->product_type == $type || ( is_array( $type ) && in_array( $this->product_type, $type ) ) ) ? true : false;
 236     }
 237 
 238     /**
 239      * Checks if a product is downloadable
 240      *
 241      * @access public
 242      * @return bool
 243      */
 244     public function is_downloadable() {
 245         return $this->downloadable == 'yes' ? true : false;
 246     }
 247 
 248     /**
 249      * Check if downloadable product has a file attached.
 250      *
 251      * @since 1.6.2
 252      *
 253      * @access public
 254      * @param string $download_id file identifier
 255      * @return bool Whether downloadable product has a file attached.
 256      */
 257     public function has_file( $download_id = '' ) {
 258         return ( $this->is_downloadable() && $this->get_file( $download_id ) ) ? true : false;
 259     }
 260 
 261     /**
 262      * Gets an array of downloadable files for this product.
 263      *
 264      * @since 2.1.0
 265      *
 266      * @return array
 267      */
 268     public function get_files() {
 269         $downloadable_files = array_filter( isset( $this->downloadable_files ) ? (array) maybe_unserialize( $this->downloadable_files ) : array() );
 270 
 271         if ( $downloadable_files ) {
 272             foreach ( $downloadable_files as $key => $file ) {
 273                 if ( ! is_array( $file ) ) {
 274                     $downloadable_files[ $key ] = array(
 275                         'file' => $file,
 276                         'name' => ''
 277                     );
 278                 }
 279 
 280                 // Set default name
 281                 if ( empty( $file['name'] ) ) {
 282                     $downloadable_files[ $key ]['name'] = wc_get_filename_from_url( $file['file'] );
 283                 }
 284 
 285                 // Filter URL
 286                 $downloadable_files[ $key ]['file'] = apply_filters( 'woocommerce_file_download_path', $downloadable_files[ $key ]['file'], $this, $key );
 287             }
 288         }
 289 
 290         return apply_filters( 'woocommerce_product_files', $downloadable_files, $this );
 291     }
 292 
 293     /**
 294      * Get a file by $download_id
 295      *
 296      * @param string $download_id file identifier
 297      * @return array|false if not found
 298      */
 299     public function get_file( $download_id = '' ) {
 300         $files = $this->get_files();
 301 
 302         if ( '' === $download_id ) {
 303             $file = sizeof( $files ) ? current( $files ) : false;
 304         } elseif ( isset( $files[ $download_id ] ) ) {
 305             $file = $files[ $download_id ];
 306         } else {
 307             $file = false;
 308         }
 309 
 310         // allow overriding based on the particular file being requested
 311         return apply_filters( 'woocommerce_product_file', $file, $this, $download_id );
 312     }
 313 
 314     /**
 315      * Get file download path identified by $download_id
 316      *
 317      * @param string $download_id file identifier
 318      * @return string
 319      */
 320     public function get_file_download_path( $download_id ) {
 321         $files = $this->get_files();
 322 
 323         if ( isset( $files[ $download_id ] ) ) {
 324             $file_path = $files[ $download_id ]['file'];
 325         } else {
 326             $file_path = '';
 327         }
 328 
 329         // allow overriding based on the particular file being requested
 330         return apply_filters( 'woocommerce_product_file_download_path', $file_path, $this, $download_id );
 331     }
 332 
 333     /**
 334      * Checks if a product is virtual (has no shipping).
 335      *
 336      * @access public
 337      * @return bool
 338      */
 339     public function is_virtual() {
 340         return $this->virtual == 'yes' ? true : false;
 341     }
 342 
 343     /**
 344      * Checks if a product needs shipping.
 345      *
 346      * @access public
 347      * @return bool
 348      */
 349     public function needs_shipping() {
 350         return apply_filters( 'woocommerce_product_needs_shipping', $this->is_virtual() ? false : true, $this );
 351     }
 352 
 353     /**
 354      * Check if a product is sold individually (no quantities)
 355      *
 356      * @access public
 357      * @return bool
 358      */
 359     public function is_sold_individually() {
 360         $return = false;
 361 
 362         if ( 'yes' == $this->sold_individually || ( ! $this->backorders_allowed() && $this->get_stock_quantity() == 1 ) ) {
 363             $return = true;
 364         }
 365 
 366         return apply_filters( 'woocommerce_is_sold_individually', $return, $this );
 367     }
 368 
 369     /**
 370      * get_children function.
 371      *
 372      * @access public
 373      * @return array
 374      */
 375     public function get_children() {
 376         return array();
 377     }
 378 
 379     /**
 380      * Returns whether or not the product has any child product.
 381      *
 382      * @access public
 383      * @return bool
 384      */
 385     public function has_child() {
 386         return false;
 387     }
 388 
 389     /**
 390      * Returns whether or not the product post exists.
 391      *
 392      * @access public
 393      * @return bool
 394      */
 395     public function exists() {
 396         return empty( $this->post ) ? false : true;
 397     }
 398 
 399     /**
 400      * Returns whether or not the product is taxable.
 401      *
 402      * @access public
 403      * @return bool
 404      */
 405     public function is_taxable() {
 406         $taxable = $this->tax_status == 'taxable' && get_option( 'woocommerce_calc_taxes' ) == 'yes' ? true : false;
 407         return apply_filters( 'woocommerce_product_is_taxable', $taxable, $this );
 408     }
 409 
 410     /**
 411      * Returns whether or not the product shipping is taxable.
 412      *
 413      * @access public
 414      * @return bool
 415      */
 416     public function is_shipping_taxable() {
 417         return $this->tax_status=='taxable' || $this->tax_status=='shipping' ? true : false;
 418     }
 419 
 420     /**
 421      * Get the title of the post.
 422      *
 423      * @access public
 424      * @return string
 425      */
 426     public function get_title() {
 427         return apply_filters( 'woocommerce_product_title', $this->post->post_title, $this );
 428     }
 429 
 430     /**
 431      * Get the parent of the post.
 432      *
 433      * @access public
 434      * @return int
 435      */
 436     public function get_parent() {
 437         return apply_filters('woocommerce_product_parent', $this->post->post_parent, $this);
 438     }
 439 
 440     /**
 441      * Get the add to url used mainly in loops.
 442      *
 443      * @access public
 444      * @return string
 445      */
 446     public function add_to_cart_url() {
 447         return apply_filters( 'woocommerce_product_add_to_cart_url', get_permalink( $this->id ), $this );
 448     }
 449 
 450     /**
 451      * Get the add to cart button text for the single page
 452      *
 453      * @access public
 454      * @return string
 455      */
 456     public function single_add_to_cart_text() {
 457         return apply_filters( 'woocommerce_product_single_add_to_cart_text', __( 'Add to cart', 'woocommerce' ), $this );
 458     }
 459 
 460     /**
 461      * Get the add to cart button text
 462      *
 463      * @access public
 464      * @return string
 465      */
 466     public function add_to_cart_text() {
 467         return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Read more', 'woocommerce' ), $this );
 468     }
 469 
 470     /**
 471      * Returns whether or not the product is stock managed.
 472      *
 473      * @access public
 474      * @return bool
 475      */
 476     public function managing_stock() {
 477         return ( ! isset( $this->manage_stock ) || $this->manage_stock == 'no' || get_option( 'woocommerce_manage_stock' ) !== 'yes' ) ? false : true;
 478     }
 479 
 480     /**
 481      * Returns whether or not the product is in stock.
 482      *
 483      * @access public
 484      * @return bool
 485      */
 486     public function is_in_stock() {
 487         if ( $this->managing_stock() ) {
 488 
 489             if ( $this->backorders_allowed() ) {
 490                 return true;
 491             } else {
 492                 if ( $this->get_total_stock() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
 493                     return false;
 494                 } else {
 495                     if ( $this->stock_status === 'instock' ) {
 496                         return true;
 497                     } else {
 498                         return false;
 499                     }
 500                 }
 501             }
 502 
 503         } else {
 504 
 505             if ( $this->stock_status === 'instock' ) {
 506                 return true;
 507             } else {
 508                 return false;
 509             }
 510 
 511         }
 512     }
 513 
 514     /**
 515      * Returns whether or not the product can be backordered.
 516      *
 517      * @access public
 518      * @return bool
 519      */
 520     public function backorders_allowed() {
 521         return $this->backorders === 'yes' || $this->backorders === 'notify' ? true : false;
 522     }
 523 
 524     /**
 525      * Returns whether or not the product needs to notify the customer on backorder.
 526      *
 527      * @access public
 528      * @return bool
 529      */
 530     public function backorders_require_notification() {
 531         return $this->managing_stock() && $this->backorders === 'notify' ? true : false;
 532     }
 533 
 534     /**
 535      * is_on_backorder function.
 536      *
 537      * @access public
 538      * @param int $qty_in_cart (default: 0)
 539      * @return bool
 540      */
 541     public function is_on_backorder( $qty_in_cart = 0 ) {
 542         return $this->managing_stock() && $this->backorders_allowed() && ( $this->get_total_stock() - $qty_in_cart ) < 0 ? true : false;
 543     }
 544 
 545     /**
 546      * Returns whether or not the product has enough stock for the order.
 547      *
 548      * @access public
 549      * @param mixed $quantity
 550      * @return bool
 551      */
 552     public function has_enough_stock( $quantity ) {
 553         return ! $this->managing_stock() || $this->backorders_allowed() || $this->stock >= $quantity ? true : false;
 554     }
 555 
 556     /**
 557      * Returns the availability of the product.
 558      *
 559      * @access public
 560      * @return string
 561      */
 562     public function get_availability() {
 563 
 564         $availability = $class = "";
 565 
 566         if ( $this->managing_stock() ) {
 567             if ( $this->is_in_stock() ) {
 568 
 569                 if ( $this->get_total_stock() > get_option( 'woocommerce_notify_no_stock_amount' ) ) {
 570 
 571                     $format_option = get_option( 'woocommerce_stock_format' );
 572 
 573                     switch ( $format_option ) {
 574                         case 'no_amount' :
 575                             $format = __( 'In stock', 'woocommerce' );
 576                         break;
 577                         case 'low_amount' :
 578                             $low_amount = get_option( 'woocommerce_notify_low_stock_amount' );
 579 
 580                             $format = ( $this->get_total_stock() <= $low_amount ) ? __( 'Only %s left in stock', 'woocommerce' ) : __( 'In stock', 'woocommerce' );
 581                         break;
 582                         default :
 583                             $format = __( '%s in stock', 'woocommerce' );
 584                         break;
 585                     }
 586 
 587                     $availability = sprintf( $format, $this->stock );
 588 
 589                     if ( $this->backorders_allowed() && $this->backorders_require_notification() ) {
 590                         $availability .= ' ' . __( '(backorders allowed)', 'woocommerce' );
 591                     }
 592 
 593                 } else {
 594 
 595                     if ( $this->backorders_allowed() ) {
 596                         if ( $this->backorders_require_notification() ) {
 597                             $availability = __( 'Available on backorder', 'woocommerce' );
 598                             $class        = 'available-on-backorder';
 599                         } else {
 600                             $availability = __( 'In stock', 'woocommerce' );
 601                         }
 602                     } else {
 603                         $availability = __( 'Out of stock', 'woocommerce' );
 604                         $class        = 'out-of-stock';
 605                     }
 606 
 607                 }
 608 
 609             } elseif ( $this->backorders_allowed() ) {
 610                 $availability = __( 'Available on backorder', 'woocommerce' );
 611                 $class        = 'available-on-backorder';
 612             } else {
 613                 $availability = __( 'Out of stock', 'woocommerce' );
 614                 $class        = 'out-of-stock';
 615             }
 616         } elseif ( ! $this->is_in_stock() ) {
 617             $availability = __( 'Out of stock', 'woocommerce' );
 618             $class        = 'out-of-stock';
 619         }
 620 
 621         return apply_filters( 'woocommerce_get_availability', array( 'availability' => $availability, 'class' => $class ), $this );
 622     }
 623 
 624     /**
 625      * Returns whether or not the product is featured.
 626      *
 627      * @access public
 628      * @return bool
 629      */
 630     public function is_featured() {
 631         return $this->featured === 'yes' ? true : false;
 632     }
 633 
 634     /**
 635      * Returns whether or not the product is visible.
 636      *
 637      * @access public
 638      * @return bool
 639      */
 640     public function is_visible() {
 641 
 642         $visible = true;
 643 
 644         // Out of stock visibility
 645         if ( get_option( 'woocommerce_hide_out_of_stock_items' ) === 'yes' && ! $this->is_in_stock() ) {
 646             $visible = false;
 647 
 648         // visibility setting
 649         } elseif ( $this->visibility === 'hidden' ) {
 650             $visible = false;
 651         } elseif ( $this->visibility === 'visible' ) {
 652             $visible = true;
 653 
 654         // Visibility in loop
 655         } elseif ( $this->visibility === 'search' && is_search() ) {
 656             $visible = true;
 657         } elseif ( $this->visibility === 'search' && ! is_search() ) {
 658             $visible = false;
 659         } elseif ( $this->visibility === 'catalog' && is_search() ) {
 660             $visible = false;
 661         } elseif ( $this->visibility === 'catalog' && ! is_search() ) {
 662             $visible = true;
 663         }
 664 
 665         return apply_filters( 'woocommerce_product_is_visible', $visible, $this->id );
 666     }
 667 
 668     /**
 669      * Returns whether or not the product is on sale.
 670      *
 671      * @access public
 672      * @return bool
 673      */
 674     public function is_on_sale() {
 675         return ( $this->get_sale_price() != $this->get_regular_price() && $this->get_sale_price() == $this->get_price() );
 676     }
 677 
 678     /**
 679      * Returns the product's weight.
 680      *
 681      * @access public
 682      * @return string
 683      */
 684     public function get_weight() {
 685         return ( $this->weight ) ? $this->weight : '';
 686     }
 687 
 688     /**
 689      * Returns false if the product cannot be bought.
 690      *
 691      * @access public
 692      * @return bool
 693      */
 694     public function is_purchasable() {
 695 
 696         $purchasable = true;
 697 
 698         // Products must exist of course
 699         if ( ! $this->exists() ) {
 700             $purchasable = false;
 701 
 702         // Other products types need a price to be set
 703         } elseif ( $this->get_price() === '' ) {
 704             $purchasable = false;
 705 
 706         // Check the product is published
 707         } elseif ( $this->post->post_status !== 'publish' && ! current_user_can( 'edit_post', $this->id ) ) {
 708             $purchasable = false;
 709         }
 710 
 711         return apply_filters( 'woocommerce_is_purchasable', $purchasable, $this );
 712     }
 713 
 714     /**
 715      * Set a products price dynamically.
 716      *
 717      * @access public
 718      * @param float $price Price to set.
 719      * @return void
 720      */
 721     public function set_price( $price ) {
 722         $this->price = $price;
 723     }
 724 
 725     /**
 726      * Adjust a products price dynamically.
 727      *
 728      * @access public
 729      * @param mixed $price
 730      * @return void
 731      */
 732     public function adjust_price( $price ) {
 733         $this->price = $this->price + $price;
 734     }
 735 
 736     /**
 737      * Returns the product's sale price.
 738      *
 739      * @return string price
 740      */
 741     public function get_sale_price() {
 742         return apply_filters( 'woocommerce_get_sale_price', $this->sale_price, $this );
 743     }
 744 
 745     /**
 746      * Returns the product's regular price.
 747      *
 748      * @return string price
 749      */
 750     public function get_regular_price() {
 751         return apply_filters( 'woocommerce_get_regular_price', $this->regular_price, $this );
 752     }
 753 
 754     /**
 755      * Returns the product's active price.
 756      *
 757      * @return string price
 758      */
 759     public function get_price() {
 760         return apply_filters( 'woocommerce_get_price', $this->price, $this );
 761     }
 762 
 763     /**
 764      * Returns the price (including tax). Uses customer tax rates. Can work for a specific $qty for more accurate taxes.
 765      *
 766      * @access public
 767      * @param  string $price to calculdate, left blank to just use get_price()
 768      * @return string
 769      */
 770     public function get_price_including_tax( $qty = 1, $price = '' ) {
 771         $_tax  = new WC_Tax();
 772 
 773         if ( ! $price ) {
 774             $price = $this->get_price();
 775         }
 776 
 777         if ( $this->is_taxable() ) {
 778 
 779             if ( get_option('woocommerce_prices_include_tax') === 'no' ) {
 780 
 781                 $tax_rates  = $_tax->get_rates( $this->get_tax_class() );
 782                 $taxes      = $_tax->calc_tax( $price * $qty, $tax_rates, false );
 783                 $tax_amount = $_tax->get_tax_total( $taxes );
 784                 $price      = round( $price * $qty + $tax_amount, absint( get_option( 'woocommerce_price_num_decimals' ) ) );
 785 
 786             } else {
 787 
 788                 $tax_rates      = $_tax->get_rates( $this->get_tax_class() );
 789                 $base_tax_rates = $_tax->get_shop_base_rate( $this->tax_class );
 790 
 791                 if ( ! empty( WC()->customer ) && WC()->customer->is_vat_exempt() ) {
 792 
 793                     $base_taxes         = $_tax->calc_tax( $price * $qty, $base_tax_rates, true );
 794                     $base_tax_amount    = array_sum( $base_taxes );
 795                     $price              = round( $price * $qty - $base_tax_amount, absint( get_option( 'woocommerce_price_num_decimals' ) ) );
 796 
 797                 } elseif ( $tax_rates !== $base_tax_rates ) {
 798 
 799                     $base_taxes         = $_tax->calc_tax( $price * $qty, $base_tax_rates, true );
 800                     $modded_taxes       = $_tax->calc_tax( ( $price * $qty ) - array_sum( $base_taxes ), $tax_rates, false );
 801                     $price              = round( ( $price * $qty ) - array_sum( $base_taxes ) + array_sum( $modded_taxes ), absint( get_option( 'woocommerce_price_num_decimals' ) ) );
 802 
 803                 } else {
 804 
 805                     $price = $price * $qty;
 806 
 807                 }
 808 
 809             }
 810 
 811         } else {
 812             $price = $price * $qty;
 813         }
 814 
 815         return apply_filters( 'woocommerce_get_price_including_tax', $price, $qty, $this );
 816     }
 817 
 818     /**
 819      * Returns the price (excluding tax) - ignores tax_class filters since the price may *include* tax and thus needs subtracting.
 820      * Uses store base tax rates. Can work for a specific $qty for more accurate taxes.
 821      *
 822      * @access public
 823      * @param  string $price to calculdate, left blank to just use get_price()
 824      * @return string
 825      */
 826     public function get_price_excluding_tax( $qty = 1, $price = '' ) {
 827 
 828         if ( ! $price ) {
 829             $price = $this->get_price();
 830         }
 831 
 832         if ( $this->is_taxable() && get_option('woocommerce_prices_include_tax') === 'yes' ) {
 833 
 834             $_tax       = new WC_Tax();
 835             $tax_rates  = $_tax->get_shop_base_rate( $this->tax_class );
 836             $taxes      = $_tax->calc_tax( $price * $qty, $tax_rates, true );
 837             $price      = $_tax->round( $price * $qty - array_sum( $taxes ) );
 838 
 839         } else {
 840             $price = $price * $qty;
 841         }
 842 
 843         return apply_filters( 'woocommerce_get_price_excluding_tax', $price, $qty, $this );
 844     }
 845 
 846     /**
 847      * Get the suffix to display after prices > 0
 848      * @return string
 849      */
 850     public function get_price_suffix() {
 851         $price_display_suffix  = get_option( 'woocommerce_price_display_suffix' );
 852 
 853         if ( $price_display_suffix ) {
 854             $price_display_suffix = ' <small class="woocommerce-price-suffix">' . $price_display_suffix . '</small>';
 855 
 856             $find = array(
 857                 '{price_including_tax}',
 858                 '{price_excluding_tax}'
 859             );
 860 
 861             $replace = array(
 862                 wc_price( $this->get_price_including_tax() ),
 863                 wc_price( $this->get_price_excluding_tax() )
 864             );
 865 
 866             $price_display_suffix = str_replace( $find, $replace, $price_display_suffix );
 867         }
 868 
 869         return apply_filters( 'woocommerce_get_price_suffix', $price_display_suffix, $this );
 870     }
 871 
 872     /**
 873      * Returns the price in html format.
 874      *
 875      * @access public
 876      * @param string $price (default: '')
 877      * @return string
 878      */
 879     public function get_price_html( $price = '' ) {
 880 
 881         $tax_display_mode      = get_option( 'woocommerce_tax_display_shop' );
 882         $display_price         = $tax_display_mode == 'incl' ? $this->get_price_including_tax() : $this->get_price_excluding_tax();
 883         $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() );
 884         $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() );
 885 
 886         if ( $this->get_price() > 0 ) {
 887 
 888             if ( $this->is_on_sale() && $this->get_regular_price() ) {
 889 
 890                 $price .= $this->get_price_html_from_to( $display_regular_price, $display_price ) . $this->get_price_suffix();
 891 
 892                 $price = apply_filters( 'woocommerce_sale_price_html', $price, $this );
 893 
 894             } else {
 895 
 896                 $price .= wc_price( $display_price ) . $this->get_price_suffix();
 897 
 898                 $price = apply_filters( 'woocommerce_price_html', $price, $this );
 899 
 900             }
 901 
 902         } elseif ( $this->get_price() === '' ) {
 903 
 904             $price = apply_filters( 'woocommerce_empty_price_html', '', $this );
 905 
 906         } elseif ( $this->get_price() == 0 ) {
 907 
 908             if ( $this->is_on_sale() && $this->get_regular_price() ) {
 909 
 910                 $price .= $this->get_price_html_from_to( $display_regular_price, __( 'Free!', 'woocommerce' ) );
 911 
 912                 $price = apply_filters( 'woocommerce_free_sale_price_html', $price, $this );
 913 
 914             } else {
 915 
 916                 $price = __( 'Free!', 'woocommerce' );
 917 
 918                 $price = apply_filters( 'woocommerce_free_price_html', $price, $this );
 919 
 920             }
 921         }
 922 
 923         return apply_filters( 'woocommerce_get_price_html', $price, $this );
 924     }
 925 
 926     /**
 927      * Functions for getting parts of a price, in html, used by get_price_html.
 928      *
 929      * @return string
 930      */
 931     public function get_price_html_from_text() {
 932         return '<span class="from">' . _x( 'From:', 'min_price', 'woocommerce' ) . ' </span>';
 933     }
 934 
 935     /**
 936      * Functions for getting parts of a price, in html, used by get_price_html.
 937      *
 938      * @param  mixed $from String or float to wrap with 'from' text
 939      * @param  mixed $to String or float to wrap with 'to' text
 940      * @return string
 941      */
 942     public function get_price_html_from_to( $from, $to ) {
 943         return '<del>' . ( ( is_numeric( $from ) ) ? wc_price( $from ) : $from ) . '</del> <ins>' . ( ( is_numeric( $to ) ) ? wc_price( $to ) : $to ) . '</ins>';
 944     }
 945 
 946     /**
 947      * Returns the tax class.
 948      *
 949      * @access public
 950      * @return string
 951      */
 952     public function get_tax_class() {
 953         return apply_filters( 'woocommerce_product_tax_class', $this->tax_class, $this );
 954     }
 955 
 956     /**
 957      * Returns the tax status.
 958      *
 959      * @access public
 960      * @return string
 961      */
 962     public function get_tax_status() {
 963         return $this->tax_status;
 964     }
 965 
 966     /**
 967      * get_average_rating function.
 968      *
 969      * @access public
 970      * @return string
 971      */
 972     public function get_average_rating() {
 973         if ( false === ( $average_rating = get_transient( 'wc_average_rating_' . $this->id ) ) ) {
 974 
 975             global $wpdb;
 976 
 977             $average_rating = '';
 978             $count          = $this->get_rating_count();
 979 
 980             if ( $count > 0 ) {
 981 
 982                 $ratings = $wpdb->get_var( $wpdb->prepare("
 983                     SELECT SUM(meta_value) FROM $wpdb->commentmeta
 984                     LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID
 985                     WHERE meta_key = 'rating'
 986                     AND comment_post_ID = %d
 987                     AND comment_approved = '1'
 988                     AND meta_value > 0
 989                 ", $this->id ) );
 990 
 991                 $average_rating = number_format( $ratings / $count, 2 );
 992 
 993             }
 994 
 995             set_transient( 'wc_average_rating_' . $this->id, $average_rating, YEAR_IN_SECONDS );
 996         }
 997 
 998         return $average_rating;
 999     }
1000 
1001     /**
1002      * get_rating_count function.
1003      *
1004      * @access public
1005      * @return int
1006      */
1007     public function get_rating_count() {
1008         if ( false === ( $count = get_transient( 'wc_rating_count_' . $this->id ) ) ) {
1009 
1010             global $wpdb;
1011 
1012             $count = $wpdb->get_var( $wpdb->prepare("
1013                 SELECT COUNT(meta_value) FROM $wpdb->commentmeta
1014                 LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID
1015                 WHERE meta_key = 'rating'
1016                 AND comment_post_ID = %d
1017                 AND comment_approved = '1'
1018                 AND meta_value > 0
1019             ", $this->id ) );
1020 
1021             set_transient( 'wc_rating_count_' . $this->id, $count, YEAR_IN_SECONDS );
1022         }
1023 
1024         return $count;
1025     }
1026 
1027     /**
1028      * Returns the product rating in html format.
1029      *
1030      * @access public
1031      * @param string $rating (default: '')
1032      * @return string
1033      */
1034     public function get_rating_html( $rating = null ) {
1035 
1036         if ( ! is_numeric( $rating ) ) {
1037             $rating = $this->get_average_rating();
1038         }
1039 
1040         if ( $rating > 0 ) {
1041 
1042             $rating_html  = '<div class="star-rating" title="' . sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $rating ) . '">';
1043 
1044             $rating_html .= '<span style="width:' . ( ( $rating / 5 ) * 100 ) . '%"><strong class="rating">' . $rating . '</strong> ' . __( 'out of 5', 'woocommerce' ) . '</span>';
1045 
1046             $rating_html .= '</div>';
1047 
1048             return $rating_html;
1049         }
1050 
1051         return '';
1052     }
1053 
1054 
1055     /**
1056      * Returns the upsell product ids.
1057      *
1058      * @access public
1059      * @return array
1060      */
1061     public function get_upsells() {
1062         return (array) maybe_unserialize( $this->upsell_ids );
1063     }
1064 
1065     /**
1066      * Returns the cross sell product ids.
1067      *
1068      * @access public
1069      * @return array
1070      */
1071     public function get_cross_sells() {
1072         return (array) maybe_unserialize( $this->crosssell_ids );
1073     }
1074 
1075     /**
1076      * Returns the product categories.
1077      *
1078      * @access public
1079      * @param string $sep (default: ')
1080      * @param mixed '
1081      * @param string $before (default: '')
1082      * @param string $after (default: '')
1083      * @return string
1084      */
1085     public function get_categories( $sep = ', ', $before = '', $after = '' ) {
1086         return get_the_term_list( $this->id, 'product_cat', $before, $sep, $after );
1087     }
1088 
1089     /**
1090      * Returns the product tags.
1091      *
1092      * @access public
1093      * @param string $sep (default: ', ')
1094      * @param string $before (default: '')
1095      * @param string $after (default: '')
1096      * @return array
1097      */
1098     public function get_tags( $sep = ', ', $before = '', $after = '' ) {
1099         return get_the_term_list( $this->id, 'product_tag', $before, $sep, $after );
1100     }
1101 
1102     /**
1103      * Returns the product shipping class.
1104      *
1105      * @access public
1106      * @return string
1107      */
1108     public function get_shipping_class() {
1109         if ( ! $this->shipping_class ) {
1110             $classes = get_the_terms( $this->id, 'product_shipping_class' );
1111 
1112             if ( $classes && ! is_wp_error( $classes ) ) {
1113                 $this->shipping_class = current( $classes )->slug;
1114             } else {
1115                 $this->shipping_class = '';
1116             }
1117 
1118         }
1119         return $this->shipping_class;
1120     }
1121 
1122     /**
1123      * Returns the product shipping class ID.
1124      *
1125      * @access public
1126      * @return int
1127      */
1128     public function get_shipping_class_id() {
1129         if ( ! $this->shipping_class_id ) {
1130             $classes = get_the_terms( $this->id, 'product_shipping_class' );
1131 
1132             if ( $classes && ! is_wp_error( $classes ) ) {
1133                 $this->shipping_class_id = current( $classes )->term_id;
1134             } else {
1135                 $this->shipping_class_id = 0;
1136             }
1137         }
1138         return absint( $this->shipping_class_id );
1139     }
1140 
1141     /**
1142      * Get and return related products.
1143      *
1144      * @access public
1145      * @param int $limit (default: 5)
1146      * @return array Array of post IDs
1147      */
1148     public function get_related( $limit = 5 ) {
1149         global $wpdb;
1150 
1151         // Related products are found from category and tag
1152         $tags_array = array(0);
1153         $cats_array = array(0);
1154 
1155         // Get tags
1156         $terms = wp_get_post_terms( $this->id, 'product_tag' );
1157         foreach ( $terms as $term ) {
1158             $tags_array[] = $term->term_id;
1159         }
1160 
1161         // Get categories
1162         $terms = wp_get_post_terms( $this->id, 'product_cat' );
1163         foreach ( $terms as $term ) {
1164             $cats_array[] = $term->term_id;
1165         }
1166 
1167         // Don't bother if none are set
1168         if ( sizeof( $cats_array ) == 1 && sizeof( $tags_array ) == 1 ) {
1169             return array();
1170         }
1171 
1172         // Sanitize
1173         $cats_array  = array_map( 'absint', $cats_array );
1174         $tags_array  = array_map( 'absint', $tags_array );
1175         $exclude_ids = array_map( 'absint', array_merge( array( 0, $this->id ), $this->get_upsells() ) );
1176 
1177         // Generate query
1178         $query['fields'] = "SELECT DISTINCT ID FROM {$wpdb->posts} p";
1179         $query['join']   = " INNER JOIN {$wpdb->postmeta} pm ON ( pm.post_id = p.ID AND pm.meta_key='_visibility' )";
1180         $query['join']  .= " INNER JOIN {$wpdb->term_relationships} tr ON (p.ID = tr.object_id)";
1181         $query['join']  .= " INNER JOIN {$wpdb->term_taxonomy} tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)";
1182         $query['join']  .= " INNER JOIN {$wpdb->terms} t ON (t.term_id = tt.term_id)";
1183 
1184         if ( get_option( 'woocommerce_hide_out_of_stock_items' ) === 'yes' ) {
1185             $query['join'] .= " INNER JOIN {$wpdb->postmeta} pm2 ON ( pm2.post_id = p.ID AND pm2.meta_key='_stock_status' )";
1186         }
1187 
1188         $query['where']  = " WHERE 1=1";
1189         $query['where'] .= " AND p.post_status = 'publish'";
1190         $query['where'] .= " AND p.post_type = 'product'";
1191         $query['where'] .= " AND p.ID NOT IN ( " . implode( ',', $exclude_ids ) . " )";
1192         $query['where'] .= " AND pm.meta_value IN ( 'visible', 'catalog' )";
1193 
1194         if ( get_option( 'woocommerce_hide_out_of_stock_items' ) === 'yes' ) {
1195             $query['where'] .= " AND pm2.meta_value = 'instock'";
1196         }
1197 
1198         if ( apply_filters( 'woocommerce_product_related_posts_relate_by_category', true ) ) {
1199             $query['where'] .= " AND ( tt.taxonomy = 'product_cat' AND t.term_id IN ( " . implode( ',', $cats_array ) . " ) )";
1200             $andor = 'OR';
1201         } else {
1202             $andor = 'AND';
1203         }
1204 
1205         // when query is OR - need to check against excluded ids again
1206         if ( apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true ) ) {
1207             $query['where'] .= " {$andor} ( tt.taxonomy = 'product_tag' AND t.term_id IN ( " . implode( ',', $tags_array ) . " ) )";
1208             $query['where'] .= " AND p.ID NOT IN ( " . implode( ',', $exclude_ids ) . " )";
1209         }
1210 
1211         $query['orderby']  = " ORDER BY RAND()";
1212         $query['limits']   = " LIMIT " . absint( $limit ) . " ";
1213 
1214         // Get the posts
1215         $related_posts = $wpdb->get_col( implode( ' ', apply_filters( 'woocommerce_product_related_posts_query', $query ) ) );
1216 
1217         return $related_posts;
1218     }
1219 
1220     /**
1221      * Returns a single product attribute.
1222      *
1223      * @access public
1224      * @param mixed $attr
1225      * @return string
1226      */
1227     public function get_attribute( $attr ) {
1228         $attributes = $this->get_attributes();
1229 
1230         $attr = sanitize_title( $attr );
1231 
1232         if ( isset( $attributes[ $attr ] ) || isset( $attributes[ 'pa_' . $attr ] ) ) {
1233 
1234             $attribute = isset( $attributes[ $attr ] ) ? $attributes[ $attr ] : $attributes[ 'pa_' . $attr ];
1235 
1236             if ( $attribute['is_taxonomy'] ) {
1237 
1238                 return implode( ', ', wc_get_product_terms( $this->id, $attribute['name'], array( 'fields' => 'names' ) ) );
1239 
1240             } else {
1241 
1242                 return $attribute['value'];
1243 
1244             }
1245 
1246         }
1247 
1248         return '';
1249     }
1250 
1251     /**
1252      * Returns product attributes.
1253      *
1254      * @access public
1255      * @return array
1256      */
1257     public function get_attributes() {
1258         return (array) maybe_unserialize( $this->product_attributes );
1259     }
1260 
1261     /**
1262      * Returns whether or not the product has any attributes set.
1263      *
1264      * @access public
1265      * @return mixed
1266      */
1267     public function has_attributes() {
1268         if ( sizeof( $this->get_attributes() ) > 0 ) {
1269             foreach ( $this->get_attributes() as $attribute ) {
1270                 if ( isset( $attribute['is_visible'] ) && $attribute['is_visible'] ) {
1271                     return true;
1272                 }
1273             }
1274         }
1275         return false;
1276     }
1277 
1278     /**
1279      * Returns whether or not we are showing dimensions on the product page.
1280      *
1281      * @access public
1282      * @return bool
1283      */
1284     public function enable_dimensions_display() {
1285         return apply_filters( 'wc_product_enable_dimensions_display', true );
1286     }
1287 
1288     /**
1289      * Returns whether or not the product has dimensions set.
1290      *
1291      * @access public
1292      * @return bool
1293      */
1294     public function has_dimensions() {
1295         return $this->get_dimensions() ? true : false;
1296     }
1297 
1298     /**
1299      * Returns whether or not the product has weight set.
1300      *
1301      * @access public
1302      * @return bool
1303      */
1304     public function has_weight() {
1305         return $this->get_weight() ? true : false;
1306     }
1307 
1308     /**
1309      * Returns dimensions.
1310      *
1311      * @access public
1312      * @return string
1313      */
1314     public function get_dimensions() {
1315         if ( ! $this->dimensions ) {
1316             $dimensions = array();
1317 
1318             if ( $this->length ) {
1319                 $dimensions[] = $this->length;
1320             }
1321 
1322             if ( $this->width ) {
1323                 $dimensions[] = $this->width;
1324             }
1325 
1326             if ( $this->height ){
1327                 $dimensions[] = $this->height;
1328             }
1329 
1330             $this->dimensions = implode( ' x ', $dimensions );
1331 
1332             if ( ! empty( $this->dimensions ) ) {
1333                 $this->dimensions .= ' ' . get_option( 'woocommerce_dimension_unit' );
1334             }
1335 
1336         }
1337         return $this->dimensions;
1338     }
1339 
1340     /**
1341      * Lists a table of attributes for the product page.
1342      *
1343      * @access public
1344      * @return void
1345      */
1346     public function list_attributes() {
1347         wc_get_template( 'single-product/product-attributes.php', array(
1348             'product'    => $this
1349         ) );
1350     }
1351 
1352     /**
1353      * Gets the main product image ID.
1354      * @return int
1355      */
1356     public function get_image_id() {
1357         if ( has_post_thumbnail( $this->id ) ) {
1358             $image_id = get_post_thumbnail_id( $this->id );
1359         } elseif ( ( $parent_id = wp_get_post_parent_id( $this->id ) ) && has_post_thumbnail( $parent_id ) ) {
1360             $image_id = get_post_thumbnail_id( $parent_id );
1361         } else {
1362             $image_id = 0;
1363         }
1364         return $image_id;
1365     }
1366 
1367     /**
1368      * Returns the main product image
1369      *
1370      * @access public
1371      * @param string $size (default: 'shop_thumbnail')
1372      * @return string
1373      */
1374     public function get_image( $size = 'shop_thumbnail', $attr = array() ) {
1375         $image = '';
1376 
1377         if ( has_post_thumbnail( $this->id ) ) {
1378             $image = get_the_post_thumbnail( $this->id, $size, $attr );
1379         } elseif ( ( $parent_id = wp_get_post_parent_id( $this->id ) ) && has_post_thumbnail( $parent_id ) ) {
1380             $image = get_the_post_thumbnail( $parent_id, $size, $attr );
1381         } else {
1382             $image = wc_placeholder_img( $size );
1383         }
1384 
1385         return $image;
1386     }
1387 
1388     /**
1389      * Get product name with SKU or ID. Used within admin.
1390      *
1391      * @access public
1392      * @param mixed $product
1393      * @return string Formatted product name
1394      */
1395     public function get_formatted_name() {
1396 
1397         if ( $this->get_sku() ) {
1398             $identifier = $this->get_sku();
1399         } else {
1400             $identifier = '#' . $this->id;
1401         }
1402 
1403         return sprintf( __( '%s &ndash; %s', 'woocommerce' ), $identifier, $this->get_title() );
1404     }
1405 }
1406 
WooCommerce API documentation generated by ApiGen 2.8.0