PageRenderTime 65ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-content/plugins/woocommerce/includes/class-wc-product-variable.php

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