/includes/wc-product-functions.php

  1. <?php 
  2. if ( ! defined( 'ABSPATH' ) ) { 
  3. exit; 
  4.  
  5. /** 
  6. * WooCommerce Product Functions 
  7. * 
  8. * Functions for product specific things. 
  9. * 
  10. * @author WooThemes 
  11. * @category Core 
  12. * @package WooCommerce/Functions 
  13. * @version 3.0.0 
  14. */ 
  15.  
  16. /** 
  17. * Products wrapper for get_posts. 
  18. * 
  19. * This function should be used for product retrieval so that we have a data agnostic 
  20. * way to get a list of products. 
  21. * 
  22. * Args: 
  23. * status array|string List of statuses to find. Default: any. Options: any, draft, pending, private and publish. 
  24. * type array|string Product type, e.g. Default: all. Options: all, simple, external, variable, variation, grouped. 
  25. * parent int post/product parent 
  26. * sku string Limit result set to products with specific SKU. 
  27. * category array Limit result set to products assigned to specific categories by slug 
  28. * e.g. array('hoodie', 'cap', 't-shirt'). 
  29. * tag array Limit result set to products assigned to specific tags (by slug) 
  30. * e.g. array('funky', 'retro', 'designer') 
  31. * shipping_class array Limit results set to products in specific shipping classes (by slug) 
  32. * e.g. array('standard', 'next-day') 
  33. * limit int Maximum of products to retrieve. 
  34. * offset int Offset of products to retrieve. 
  35. * page int Page of products to retrieve. Ignored when using the 'offset' arg. 
  36. * exclude array Product IDs to exclude from the query. 
  37. * orderby string Order by date, title, id, modified, rand etc 
  38. * order string ASC or DESC 
  39. * return string Type of data to return. Allowed values: 
  40. * ids array of Product ids 
  41. * objects array of product objects (default) 
  42. * paginate bool If true, the return value will be an array with values: 
  43. * 'products' => array of data (return value above),  
  44. * 'total' => total number of products matching the query 
  45. * 'max_num_pages' => max number of pages found 
  46. * 
  47. * @since 3.0.0 
  48. * @param array $args Array of args (above) 
  49. * @return array|stdClass Number of pages and an array of product objects if 
  50. * paginate is true, or just an array of values. 
  51. */ 
  52. function wc_get_products( $args ) { 
  53. $args = wp_parse_args( $args, array( 
  54. 'status' => array( 'draft', 'pending', 'private', 'publish' ),  
  55. 'type' => array_merge( array_keys( wc_get_product_types() ) ),  
  56. 'parent' => null,  
  57. 'sku' => '',  
  58. 'category' => array(),  
  59. 'tag' => array(),  
  60. 'limit' => get_option( 'posts_per_page' ),  
  61. 'offset' => null,  
  62. 'page' => 1,  
  63. 'include' => array(),  
  64. 'exclude' => array(),  
  65. 'orderby' => 'date',  
  66. 'order' => 'DESC',  
  67. 'return' => 'objects',  
  68. 'paginate' => false,  
  69. 'shipping_class' => array(),  
  70. ) ); 
  71.  
  72. // Handle some BW compatibility arg names where wp_query args differ in naming. 
  73. $map_legacy = array( 
  74. 'numberposts' => 'limit',  
  75. 'post_status' => 'status',  
  76. 'post_parent' => 'parent',  
  77. 'posts_per_page' => 'limit',  
  78. 'paged' => 'page',  
  79. ); 
  80.  
  81. foreach ( $map_legacy as $from => $to ) { 
  82. if ( isset( $args[ $from ] ) ) { 
  83. $args[ $to ] = $args[ $from ]; 
  84.  
  85. return WC_Data_Store::load( 'product' )->get_products( $args ); 
  86.  
  87. /** 
  88. * Main function for returning products, uses the WC_Product_Factory class. 
  89. * 
  90. * @since 2.2.0 
  91. * 
  92. * @param mixed $the_product Post object or post ID of the product. 
  93. * @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type. 
  94. * @return WC_Product|null 
  95. */ 
  96. function wc_get_product( $the_product = false, $deprecated = array() ) { 
  97. if ( ! did_action( 'woocommerce_init' ) ) { 
  98. wc_doing_it_wrong( __FUNCTION__, __( 'wc_get_product should not be called before the woocommerce_init action.', 'woocommerce' ), '2.5' ); 
  99. return false; 
  100. if ( ! empty( $deprecated ) ) { 
  101. wc_deprecated_argument( 'args', '3.0', 'Passing args to wc_get_product is deprecated. If you need to force a type, construct the product class directly.' ); 
  102. return WC()->product_factory->get_product( $the_product, $deprecated ); 
  103.  
  104. /** 
  105. * Returns whether or not SKUS are enabled. 
  106. * @return bool 
  107. */ 
  108. function wc_product_sku_enabled() { 
  109. return apply_filters( 'wc_product_sku_enabled', true ); 
  110.  
  111. /** 
  112. * Returns whether or not product weights are enabled. 
  113. * @return bool 
  114. */ 
  115. function wc_product_weight_enabled() { 
  116. return apply_filters( 'wc_product_weight_enabled', true ); 
  117.  
  118. /** 
  119. * Returns whether or not product dimensions (HxWxD) are enabled. 
  120. * @return bool 
  121. */ 
  122. function wc_product_dimensions_enabled() { 
  123. return apply_filters( 'wc_product_dimensions_enabled', true ); 
  124.  
  125. /** 
  126. * Clear all transients cache for product data. 
  127. * 
  128. * @param int $post_id (default: 0) 
  129. */ 
  130. function wc_delete_product_transients( $post_id = 0 ) { 
  131. // Core transients 
  132. $transients_to_clear = array( 
  133. 'wc_products_onsale',  
  134. 'wc_featured_products',  
  135. 'wc_outofstock_count',  
  136. 'wc_low_stock_count',  
  137. ); 
  138.  
  139. // Transient names that include an ID 
  140. $post_transient_names = array( 
  141. 'wc_product_children_',  
  142. 'wc_var_prices_',  
  143. 'wc_related_',  
  144. 'wc_child_has_weight_',  
  145. 'wc_child_has_dimensions_',  
  146. ); 
  147.  
  148. if ( $post_id > 0 ) { 
  149. foreach ( $post_transient_names as $transient ) { 
  150. $transients_to_clear[] = $transient . $post_id; 
  151.  
  152. // Does this product have a parent? 
  153. $product = wc_get_product( $post_id ); 
  154.  
  155. if ( $product && $product->get_parent_id() > 0 ) { 
  156. wc_delete_product_transients( $product->get_parent_id() ); 
  157.  
  158. // Delete transients 
  159. foreach ( $transients_to_clear as $transient ) { 
  160. delete_transient( $transient ); 
  161.  
  162. // Increments the transient version to invalidate cache 
  163. WC_Cache_Helper::get_transient_version( 'product', true ); 
  164.  
  165. do_action( 'woocommerce_delete_product_transients', $post_id ); 
  166.  
  167. /** 
  168. * Function that returns an array containing the IDs of the products that are on sale. 
  169. * 
  170. * @since 2.0 
  171. * @return array 
  172. */ 
  173. function wc_get_product_ids_on_sale() { 
  174. // Load from cache 
  175. $product_ids_on_sale = get_transient( 'wc_products_onsale' ); 
  176.  
  177. // Valid cache found 
  178. if ( false !== $product_ids_on_sale ) { 
  179. return $product_ids_on_sale; 
  180.  
  181. $data_store = WC_Data_Store::load( 'product' ); 
  182. $on_sale_products = $data_store->get_on_sale_products(); 
  183. $product_ids_on_sale = wp_parse_id_list( array_merge( wp_list_pluck( $on_sale_products, 'id' ), array_diff( wp_list_pluck( $on_sale_products, 'parent_id' ), array( 0 ) ) ) ); 
  184.  
  185. set_transient( 'wc_products_onsale', $product_ids_on_sale, DAY_IN_SECONDS * 30 ); 
  186.  
  187. return $product_ids_on_sale; 
  188.  
  189. /** 
  190. * Function that returns an array containing the IDs of the featured products. 
  191. * 
  192. * @since 2.1 
  193. * @return array 
  194. */ 
  195. function wc_get_featured_product_ids() { 
  196. // Load from cache 
  197. $featured_product_ids = get_transient( 'wc_featured_products' ); 
  198.  
  199. // Valid cache found 
  200. if ( false !== $featured_product_ids ) 
  201. return $featured_product_ids; 
  202.  
  203. $data_store = WC_Data_Store::load( 'product' ); 
  204. $featured = $data_store->get_featured_product_ids(); 
  205. $product_ids = array_keys( $featured ); 
  206. $parent_ids = array_values( array_filter( $featured ) ); 
  207. $featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) ); 
  208.  
  209. set_transient( 'wc_featured_products', $featured_product_ids, DAY_IN_SECONDS * 30 ); 
  210.  
  211. return $featured_product_ids; 
  212.  
  213. /** 
  214. * Filter to allow product_cat in the permalinks for products. 
  215. * 
  216. * @param string $permalink The existing permalink URL. 
  217. * @param WP_Post $post 
  218. * @return string 
  219. */ 
  220. function wc_product_post_type_link( $permalink, $post ) { 
  221. // Abort if post is not a product. 
  222. if ( 'product' !== $post->post_type ) { 
  223. return $permalink; 
  224.  
  225. // Abort early if the placeholder rewrite tag isn't in the generated URL. 
  226. if ( false === strpos( $permalink, '%' ) ) { 
  227. return $permalink; 
  228.  
  229. // Get the custom taxonomy terms in use by this post. 
  230. $terms = get_the_terms( $post->ID, 'product_cat' ); 
  231.  
  232. if ( ! empty( $terms ) ) { 
  233. if ( function_exists( 'wp_list_sort' ) ) { 
  234. $terms = wp_list_sort( $terms, 'term_id', 'ASC' ); 
  235. } else { 
  236. usort( $terms, '_usort_terms_by_ID' ); 
  237. $category_object = apply_filters( 'wc_product_post_type_link_product_cat', $terms[0], $terms, $post ); 
  238. $category_object = get_term( $category_object, 'product_cat' ); 
  239. $product_cat = $category_object->slug; 
  240.  
  241. if ( $category_object->parent ) { 
  242. $ancestors = get_ancestors( $category_object->term_id, 'product_cat' ); 
  243. foreach ( $ancestors as $ancestor ) { 
  244. $ancestor_object = get_term( $ancestor, 'product_cat' ); 
  245. $product_cat = $ancestor_object->slug . '/' . $product_cat; 
  246. } else { 
  247. // If no terms are assigned to this post, use a string instead (can't leave the placeholder there) 
  248. $product_cat = _x( 'uncategorized', 'slug', 'woocommerce' ); 
  249.  
  250. $find = array( 
  251. '%year%',  
  252. '%monthnum%',  
  253. '%day%',  
  254. '%hour%',  
  255. '%minute%',  
  256. '%second%',  
  257. '%post_id%',  
  258. '%category%',  
  259. '%product_cat%',  
  260. ); 
  261.  
  262. $replace = array( 
  263. date_i18n( 'Y', strtotime( $post->post_date ) ),  
  264. date_i18n( 'm', strtotime( $post->post_date ) ),  
  265. date_i18n( 'd', strtotime( $post->post_date ) ),  
  266. date_i18n( 'H', strtotime( $post->post_date ) ),  
  267. date_i18n( 'i', strtotime( $post->post_date ) ),  
  268. date_i18n( 's', strtotime( $post->post_date ) ),  
  269. $post->ID,  
  270. $product_cat,  
  271. $product_cat,  
  272. ); 
  273.  
  274. $permalink = str_replace( $find, $replace, $permalink ); 
  275.  
  276. return $permalink; 
  277. add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 ); 
  278.  
  279.  
  280. /** 
  281. * Get the placeholder image URL for products etc. 
  282. * 
  283. * @access public 
  284. * @return string 
  285. */ 
  286. function wc_placeholder_img_src() { 
  287. return apply_filters( 'woocommerce_placeholder_img_src', WC()->plugin_url() . '/assets/images/placeholder.png' ); 
  288.  
  289. /** 
  290. * Get the placeholder image. 
  291. * 
  292. * @access public 
  293. * @return string 
  294. */ 
  295. function wc_placeholder_img( $size = 'shop_thumbnail' ) { 
  296. $dimensions = wc_get_image_size( $size ); 
  297.  
  298. return apply_filters( 'woocommerce_placeholder_img', '<img src="' . wc_placeholder_img_src() . '" alt="' . esc_attr__( 'Placeholder', 'woocommerce' ) . '" width="' . esc_attr( $dimensions['width'] ) . '" class="woocommerce-placeholder wp-post-image" height="' . esc_attr( $dimensions['height'] ) . '" />', $size, $dimensions ); 
  299.  
  300. /** 
  301. * Variation Formatting. 
  302. * 
  303. * Gets a formatted version of variation data or item meta. 
  304. * 
  305. * @param array|WC_Product_Variation $variation 
  306. * @param bool $flat (default: false) 
  307. * @param bool $include_names include attribute names/labels 
  308. * @return string 
  309. */ 
  310. function wc_get_formatted_variation( $variation, $flat = false, $include_names = true ) { 
  311. $return = ''; 
  312.  
  313. if ( is_a( $variation, 'WC_Product_Variation' ) ) { 
  314. $variation_attributes = $variation->get_attributes(); 
  315. $product = $variation; 
  316. } else { 
  317. $product = false; 
  318. // Remove attribute_ prefix from names. 
  319. $variation_attributes = array(); 
  320. if ( is_array( $variation ) ) { 
  321. foreach ( $variation as $key => $value ) { 
  322. $variation_attributes[ str_replace( 'attribute_', '', $key ) ] = $value; 
  323.  
  324. $list_type = $include_names ? 'dl' : 'ul'; 
  325.  
  326. if ( is_array( $variation_attributes ) ) { 
  327.  
  328. if ( ! $flat ) { 
  329. $return = '<' . $list_type . ' class="variation">'; 
  330.  
  331. $variation_list = array(); 
  332.  
  333. foreach ( $variation_attributes as $name => $value ) { 
  334. if ( ! $value ) { 
  335. continue; 
  336.  
  337. // If this is a term slug, get the term's nice name 
  338. if ( taxonomy_exists( $name ) ) { 
  339. $term = get_term_by( 'slug', $value, $name ); 
  340. if ( ! is_wp_error( $term ) && ! empty( $term->name ) ) { 
  341. $value = $term->name; 
  342. } else { 
  343. $value = ucwords( str_replace( '-', ' ', $value ) ); 
  344.  
  345. if ( $include_names ) { 
  346. if ( $flat ) { 
  347. $variation_list[] = wc_attribute_label( $name, $product ) . ': ' . rawurldecode( $value ); 
  348. } else { 
  349. $variation_list[] = '<dt>' . wc_attribute_label( $name, $product ) . ':</dt><dd>' . rawurldecode( $value ) . '</dd>'; 
  350. } else { 
  351. if ( $flat ) { 
  352. $variation_list[] = rawurldecode( $value ); 
  353. } else { 
  354. $variation_list[] = '<li>' . rawurldecode( $value ) . '</li>'; 
  355.  
  356. if ( $flat ) { 
  357. $return .= implode( ', ', $variation_list ); 
  358. } else { 
  359. $return .= implode( '', $variation_list ); 
  360.  
  361. if ( ! $flat ) { 
  362. $return .= '</' . $list_type . '>'; 
  363. return $return; 
  364.  
  365. /** 
  366. * Function which handles the start and end of scheduled sales via cron. 
  367. */ 
  368. function wc_scheduled_sales() { 
  369. $data_store = WC_Data_Store::load( 'product' ); 
  370.  
  371. // Sales which are due to start 
  372. $product_ids = $data_store->get_starting_sales(); 
  373. if ( $product_ids ) { 
  374. foreach ( $product_ids as $product_id ) { 
  375. $product = wc_get_product( $product_id ); 
  376. $sale_price = $product->get_sale_price(); 
  377.  
  378. if ( $sale_price ) { 
  379. $product->set_price( $sale_price ); 
  380. } else { 
  381. $product->set_date_on_sale_to( '' ); 
  382. $product->set_date_on_sale_from( '' ); 
  383.  
  384. $product->save(); 
  385.  
  386. delete_transient( 'wc_products_onsale' ); 
  387.  
  388. // Sales which are due to end 
  389. $product_ids = $data_store->get_ending_sales(); 
  390. if ( $product_ids ) { 
  391. foreach ( $product_ids as $product_id ) { 
  392. $product = wc_get_product( $product_id ); 
  393. $regular_price = $product->get_regular_price(); 
  394. $product->set_price( $regular_price ); 
  395. $product->set_sale_price( '' ); 
  396. $product->set_date_on_sale_to( '' ); 
  397. $product->set_date_on_sale_from( '' ); 
  398. $product->save(); 
  399.  
  400. WC_Cache_Helper::get_transient_version( 'product', true ); 
  401. delete_transient( 'wc_products_onsale' ); 
  402. add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' ); 
  403.  
  404. /** 
  405. * Get attachment image attributes. 
  406. * 
  407. * @access public 
  408. * @param array $attr 
  409. * @return array 
  410. */ 
  411. function wc_get_attachment_image_attributes( $attr ) { 
  412. if ( strstr( $attr['src'][0], 'woocommerce_uploads/' ) ) { 
  413. $attr['src'][0] = wc_placeholder_img_src(); 
  414. return $attr; 
  415. add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' ); 
  416.  
  417.  
  418. /** 
  419. * Prepare attachment for JavaScript. 
  420. * 
  421. * @access public 
  422. * @param array $response 
  423. * @return array 
  424. */ 
  425. function wc_prepare_attachment_for_js( $response ) { 
  426.  
  427. if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) { 
  428. $response['full']['url'] = wc_placeholder_img_src(); 
  429. if ( isset( $response['sizes'] ) ) { 
  430. foreach ( $response['sizes'] as $size => $value ) { 
  431. $response['sizes'][ $size ]['url'] = wc_placeholder_img_src(); 
  432.  
  433. return $response; 
  434. add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' ); 
  435.  
  436. /** 
  437. * Track product views. 
  438. */ 
  439. function wc_track_product_view() { 
  440. if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) { 
  441. return; 
  442.  
  443. global $post; 
  444.  
  445. if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) ) 
  446. $viewed_products = array(); 
  447. else 
  448. $viewed_products = (array) explode( '|', $_COOKIE['woocommerce_recently_viewed'] ); 
  449.  
  450. if ( ! in_array( $post->ID, $viewed_products ) ) { 
  451. $viewed_products[] = $post->ID; 
  452.  
  453. if ( sizeof( $viewed_products ) > 15 ) { 
  454. array_shift( $viewed_products ); 
  455.  
  456. // Store for session only 
  457. wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) ); 
  458.  
  459. add_action( 'template_redirect', 'wc_track_product_view', 20 ); 
  460.  
  461. /** 
  462. * Get product types. 
  463. * 
  464. * @since 2.2 
  465. * @return array 
  466. */ 
  467. function wc_get_product_types() { 
  468. return (array) apply_filters( 'product_type_selector', array( 
  469. 'simple' => __( 'Simple product', 'woocommerce' ),  
  470. 'grouped' => __( 'Grouped product', 'woocommerce' ),  
  471. 'external' => __( 'External/Affiliate product', 'woocommerce' ),  
  472. 'variable' => __( 'Variable product', 'woocommerce' ),  
  473. ) ); 
  474.  
  475. /** 
  476. * Check if product sku is unique. 
  477. * 
  478. * @since 2.2 
  479. * @param int $product_id 
  480. * @param string $sku 
  481. * @return bool 
  482. */ 
  483. function wc_product_has_unique_sku( $product_id, $sku ) { 
  484. $data_store = WC_Data_Store::load( 'product' ); 
  485. $sku_found = $data_store->is_existing_sku( $product_id, $sku ); 
  486.  
  487. if ( apply_filters( 'wc_product_has_unique_sku', $sku_found, $product_id, $sku ) ) { 
  488. return false; 
  489. } else { 
  490. return true; 
  491.  
  492. /** 
  493. * Force a unique SKU. 
  494. * 
  495. * @since 3.0.0 
  496. * @param integer $product_id 
  497. */ 
  498. function wc_product_force_unique_sku( $product_id ) { 
  499. $product = wc_get_product( $product_id ); 
  500.  
  501. if ( $product && ( $current_sku = $product->get_sku( 'edit' ) ) ) { 
  502. try { 
  503. $new_sku = wc_product_generate_unique_sku( $product_id, $current_sku ); 
  504.  
  505. if ( $current_sku !== $new_sku ) { 
  506. $product->set_sku( $new_sku ); 
  507. $product->save(); 
  508. } catch ( Exception $e ) {} 
  509.  
  510. /** 
  511. * Recursively appends a suffix until a unique SKU is found. 
  512. * 
  513. * @since 3.0.0 
  514. * @param integer $product_id 
  515. * @param string $sku 
  516. * @param integer $index 
  517. * @return string 
  518. */ 
  519. function wc_product_generate_unique_sku( $product_id, $sku, $index = 0 ) { 
  520. $generated_sku = 0 < $index ? $sku . '-' . $index : $sku; 
  521.  
  522. if ( ! wc_product_has_unique_sku( $product_id, $generated_sku ) ) { 
  523. $generated_sku = wc_product_generate_unique_sku( $product_id, $sku, ( $index + 1 ) ); 
  524.  
  525. return $generated_sku; 
  526.  
  527. /** 
  528. * Get product ID by SKU. 
  529. * 
  530. * @since 2.3.0 
  531. * @param string $sku 
  532. * @return int 
  533. */ 
  534. function wc_get_product_id_by_sku( $sku ) { 
  535. global $wpdb; 
  536.  
  537. $data_store = WC_Data_Store::load( 'product' ); 
  538. $product_id = $data_store->get_product_id_by_sku( $sku ); 
  539.  
  540. return ( $product_id ) ? intval( $product_id ) : 0; 
  541.  
  542. /** 
  543. * Get attibutes/data for an individual variation from the database and maintain it's integrity. 
  544. * @param revisit? 
  545. * @since 2.4.0 
  546. * @param int $variation_id 
  547. * @return array 
  548. */ 
  549. function wc_get_product_variation_attributes( $variation_id ) { 
  550. // Build variation data from meta 
  551. $all_meta = get_post_meta( $variation_id ); 
  552. $parent_id = wp_get_post_parent_id( $variation_id ); 
  553. $parent_attributes = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) ); 
  554. $found_parent_attributes = array(); 
  555. $variation_attributes = array(); 
  556.  
  557. // Compare to parent variable product attributes and ensure they match 
  558. foreach ( $parent_attributes as $attribute_name => $options ) { 
  559. if ( ! empty( $options['is_variation'] ) ) { 
  560. $attribute = 'attribute_' . sanitize_title( $attribute_name ); 
  561. $found_parent_attributes[] = $attribute; 
  562. if ( ! array_key_exists( $attribute, $variation_attributes ) ) { 
  563. $variation_attributes[ $attribute ] = ''; // Add it - 'any' will be asumed 
  564.  
  565. // Get the variation attributes from meta 
  566. foreach ( $all_meta as $name => $value ) { 
  567. // Only look at valid attribute meta, and also compare variation level attributes and remove any which do not exist at parent level 
  568. if ( 0 !== strpos( $name, 'attribute_' ) || ! in_array( $name, $found_parent_attributes ) ) { 
  569. unset( $variation_attributes[ $name ] ); 
  570. continue; 
  571. /** 
  572. * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. 
  573. * Attempt to get full version of the text attribute from the parent. 
  574. */ 
  575. if ( sanitize_title( $value[0] ) === $value[0] && version_compare( get_post_meta( $parent_id, '_product_version', true ), '2.4.0', '<' ) ) { 
  576. foreach ( $parent_attributes as $attribute ) { 
  577. if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) { 
  578. continue; 
  579. $text_attributes = wc_get_text_attributes( $attribute['value'] ); 
  580.  
  581. foreach ( $text_attributes as $text_attribute ) { 
  582. if ( sanitize_title( $text_attribute ) === $value[0] ) { 
  583. $value[0] = $text_attribute; 
  584. break; 
  585.  
  586. $variation_attributes[ $name ] = $value[0]; 
  587.  
  588. return $variation_attributes; 
  589.  
  590. /** 
  591. * Get all product cats for a product by ID, including hierarchy 
  592. * @since 2.5.0 
  593. * @param int $product_id 
  594. * @return array 
  595. */ 
  596. function wc_get_product_cat_ids( $product_id ) { 
  597. $product_cats = wc_get_product_term_ids( $product_id, 'product_cat' ); 
  598.  
  599. foreach ( $product_cats as $product_cat ) { 
  600. $product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) ); 
  601.  
  602. return $product_cats; 
  603.  
  604. /** 
  605. * Gets data about an attachment, such as alt text and captions. 
  606. * @since 2.6.0 
  607. * @param object|bool $product 
  608. * @return array 
  609. */ 
  610. function wc_get_product_attachment_props( $attachment_id = null, $product = false ) { 
  611. $props = array( 
  612. 'title' => '',  
  613. 'caption' => '',  
  614. 'url' => '',  
  615. 'alt' => '',  
  616. 'src' => '',  
  617. 'srcset' => false,  
  618. 'sizes' => false,  
  619. ); 
  620. if ( $attachment = get_post( $attachment_id ) ) { 
  621. $props['title'] = trim( strip_tags( $attachment->post_title ) ); 
  622. $props['caption'] = trim( strip_tags( $attachment->post_excerpt ) ); 
  623. $props['url'] = wp_get_attachment_url( $attachment_id ); 
  624. $props['alt'] = trim( strip_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) ); 
  625.  
  626. // Large version. 
  627. $src = wp_get_attachment_image_src( $attachment_id, 'large' ); 
  628. $props['full_src'] = $src[0]; 
  629. $props['full_src_w'] = $src[1]; 
  630. $props['full_src_h'] = $src[2]; 
  631.  
  632. // Thumbnail version. 
  633. $src = wp_get_attachment_image_src( $attachment_id, 'shop_thumbnail' ); 
  634. $props['thumb_src'] = $src[0]; 
  635. $props['thumb_src_w'] = $src[1]; 
  636. $props['thumb_src_h'] = $src[2]; 
  637.  
  638. // Image source. 
  639. $src = wp_get_attachment_image_src( $attachment_id, 'shop_single' ); 
  640. $props['src'] = $src[0]; 
  641. $props['src_w'] = $src[1]; 
  642. $props['src_h'] = $src[2]; 
  643. $props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, 'shop_single' ) : false; 
  644. $props['sizes'] = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, 'shop_single' ) : false; 
  645.  
  646. // Alt text fallbacks 
  647. $props['alt'] = empty( $props['alt'] ) ? $props['caption'] : $props['alt']; 
  648. $props['alt'] = empty( $props['alt'] ) ? trim( strip_tags( $attachment->post_title ) ) : $props['alt']; 
  649. $props['alt'] = empty( $props['alt'] ) && $product ? trim( strip_tags( get_the_title( $product->ID ) ) ) : $props['alt']; 
  650. return $props; 
  651.  
  652. /** 
  653. * Get product visibility options. 
  654. * 
  655. * @since 3.0.0 
  656. * @return array 
  657. */ 
  658. function wc_get_product_visibility_options() { 
  659. return apply_filters( 'woocommerce_product_visibility_options', array( 
  660. 'visible' => __( 'Visible', 'woocommerce' ),  
  661. 'catalog' => __( 'Catalog', 'woocommerce' ),  
  662. 'search' => __( 'Search', 'woocommerce' ),  
  663. 'hidden' => __( 'Hidden', 'woocommerce' ),  
  664. ) ); 
  665.  
  666. /** 
  667. * Get min/max price meta query args. 
  668. * 
  669. * @since 3.0.0 
  670. * @param array $args Min price and max price arguments. 
  671. * @return array 
  672. */ 
  673. function wc_get_min_max_price_meta_query( $args ) { 
  674. $min = isset( $args['min_price'] ) ? floatval( $args['min_price'] ) : 0; 
  675. $max = isset( $args['max_price'] ) ? floatval( $args['max_price'] ) : 9999999999; 
  676.  
  677. /** 
  678. * Adjust if the store taxes are not displayed how they are stored. 
  679. * Max is left alone because the filter was already increased. 
  680. * Kicks in when prices excluding tax are displayed including tax. 
  681. */ 
  682. if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) && ! wc_prices_include_tax() ) { 
  683. $tax_classes = array_merge( array( '' ), WC_Tax::get_tax_classes() ); 
  684. $class_min = $min; 
  685.  
  686. foreach ( $tax_classes as $tax_class ) { 
  687. if ( $tax_rates = WC_Tax::get_rates( $tax_class ) ) { 
  688. $class_min = $min - WC_Tax::get_tax_total( WC_Tax::calc_exclusive_tax( $min, $tax_rates ) ); 
  689.  
  690. $min = $class_min; 
  691.  
  692. return array( 
  693. 'key' => '_price',  
  694. 'value' => array( $min, $max ),  
  695. 'compare' => 'BETWEEN',  
  696. 'type' => 'DECIMAL',  
  697. ); 
  698.  
  699. /** 
  700. * Get product tax class options. 
  701. * 
  702. * @since 3.0.0 
  703. * @return array 
  704. */ 
  705. function wc_get_product_tax_class_options() { 
  706. $tax_classes = WC_Tax::get_tax_classes(); 
  707. $tax_class_options = array(); 
  708. $tax_class_options[''] = __( 'Standard', 'woocommerce' ); 
  709.  
  710. if ( ! empty( $tax_classes ) ) { 
  711. foreach ( $tax_classes as $class ) { 
  712. $tax_class_options[ sanitize_title( $class ) ] = $class; 
  713. return $tax_class_options; 
  714.  
  715. /** 
  716. * Get stock status options. 
  717. * 
  718. * @since 3.0.0 
  719. * @return array 
  720. */ 
  721. function wc_get_product_stock_status_options() { 
  722. return array( 
  723. 'instock' => __( 'In stock', 'woocommerce' ),  
  724. 'outofstock' => __( 'Out of stock', 'woocommerce' ),  
  725. ); 
  726.  
  727. /** 
  728. * Get backorder options. 
  729. * 
  730. * @since 3.0.0 
  731. * @return array 
  732. */ 
  733. function wc_get_product_backorder_options() { 
  734. return array( 
  735. 'no' => __( 'Do not allow', 'woocommerce' ),  
  736. 'notify' => __( 'Allow, but notify customer', 'woocommerce' ),  
  737. 'yes' => __( 'Allow', 'woocommerce' ),  
  738. ); 
  739.  
  740. /** 
  741. * Get related products based on product category and tags. 
  742. * 
  743. * @since 3.0.0 
  744. * @param int $product_id Product ID. 
  745. * @param int $limit Limit of results. 
  746. * @param array $exclude_ids Exclude IDs from the results. 
  747. * @return array 
  748. */ 
  749. function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() ) { 
  750. global $wpdb; 
  751.  
  752. $product_id = absint( $product_id ); 
  753. $exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids ); 
  754. $transient_name = 'wc_related_' . $product_id; 
  755. $related_posts = get_transient( $transient_name ); 
  756. $limit = $limit > 0 ? $limit : 5; 
  757.  
  758. // We want to query related posts if they are not cached, or we don't have enough. 
  759. if ( false === $related_posts || count( $related_posts ) < $limit ) { 
  760. $cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array(); 
  761. $tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array(); 
  762.  
  763. // Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related. 
  764. if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) { 
  765. $related_posts = array(); 
  766. } else { 
  767. $data_store = WC_Data_Store::load( 'product' ); 
  768. $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id ); 
  769.  
  770. set_transient( $transient_name, $related_posts, DAY_IN_SECONDS ); 
  771.  
  772. shuffle( $related_posts ); 
  773.  
  774. return array_slice( $related_posts, 0, $limit ); 
  775.  
  776. /** 
  777. * Retrieves product term ids for a taxonomy. 
  778. * 
  779. * @since 3.0.0 
  780. * @param int $product_id Product ID. 
  781. * @param string $taxonomy Taxonomy slug. 
  782. * @return array 
  783. */ 
  784. function wc_get_product_term_ids( $product_id, $taxonomy ) { 
  785. $terms = get_the_terms( $product_id, $taxonomy ); 
  786. return ! empty( $terms ) ? wp_list_pluck( $terms, 'term_id' ) : array(); 
  787.  
  788. /** 
  789. * For a given product, and optionally price/qty, work out the price with tax included, based on store settings. 
  790. * @since 3.0.0 
  791. * @param WC_Product $product 
  792. * @param array $args 
  793. * @return float 
  794. */ 
  795. function wc_get_price_including_tax( $product, $args = array() ) { 
  796. $args = wp_parse_args( $args, array( 
  797. 'qty' => '',  
  798. 'price' => '',  
  799. ) ); 
  800.  
  801. $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); 
  802. $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; 
  803.  
  804. if ( '' === $price ) { 
  805. return ''; 
  806. } elseif ( empty( $qty ) ) { 
  807. return 0.0; 
  808.  
  809. $line_price = $price * $qty; 
  810. $return_price = $line_price; 
  811.  
  812. if ( $product->is_taxable() ) { 
  813. if ( ! wc_prices_include_tax() ) { 
  814. $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); 
  815. $taxes = WC_Tax::calc_tax( $line_price, $tax_rates, false ); 
  816. $tax_amount = WC_Tax::get_tax_total( $taxes ); 
  817. $return_price = round( $line_price + $tax_amount, wc_get_price_decimals() ); 
  818. } else { 
  819. $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); 
  820. $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); 
  821.  
  822. /** 
  823. * If the customer is excempt from VAT, remove the taxes here. 
  824. * Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting. 
  825. */ 
  826. if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { 
  827. $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); 
  828. $remove_tax = array_sum( $remove_taxes ); 
  829. $return_price = round( $line_price - $remove_tax, wc_get_price_decimals() ); 
  830.  
  831. /** 
  832. * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. 
  833. * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. 
  834. * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. 
  835. */ 
  836. } elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { 
  837. $base_taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true ); 
  838. $modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false ); 
  839. $return_price = round( $line_price - array_sum( $base_taxes ) + array_sum( $modded_taxes ), wc_get_price_decimals() ); 
  840. return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product ); 
  841.  
  842. /** 
  843. * For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings. 
  844. * @since 3.0.0 
  845. * @param WC_Product $product 
  846. * @param array $args 
  847. * @return float 
  848. */ 
  849. function wc_get_price_excluding_tax( $product, $args = array() ) { 
  850. $args = wp_parse_args( $args, array( 
  851. 'qty' => '',  
  852. 'price' => '',  
  853. ) ); 
  854.  
  855. $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); 
  856. $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; 
  857.  
  858. if ( '' === $price ) { 
  859. return ''; 
  860. } elseif ( empty( $qty ) ) { 
  861. return 0.0; 
  862.  
  863. if ( $product->is_taxable() && wc_prices_include_tax() ) { 
  864. $tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); 
  865. $taxes = WC_Tax::calc_tax( $price * $qty, $tax_rates, true ); 
  866. $price = WC_Tax::round( $price * $qty - array_sum( $taxes ) ); 
  867. } else { 
  868. $price = $price * $qty; 
  869.  
  870. return apply_filters( 'woocommerce_get_price_excluding_tax', $price, $qty, $product ); 
  871.  
  872. /** 
  873. * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting. 
  874. * @since 3.0.0 
  875. * @param WC_Product $product 
  876. * @param array $args 
  877. * @return float 
  878. */ 
  879. function wc_get_price_to_display( $product, $args = array() ) { 
  880. $args = wp_parse_args( $args, array( 
  881. 'qty' => 1,  
  882. 'price' => $product->get_price(),  
  883. ) ); 
  884.  
  885. $price = $args['price']; 
  886. $qty = $args['qty']; 
  887.  
  888. return 'incl' === get_option( 'woocommerce_tax_display_shop' ) ? wc_get_price_including_tax( $product, array( 'qty' => $qty, 'price' => $price ) ) : wc_get_price_excluding_tax( $product, array( 'qty' => $qty, 'price' => $price ) ); 
  889.  
  890. /** 
  891. * Returns the product categories in a list. 
  892. * 
  893. * @param int $product_id 
  894. * @param string $sep (default: ', '). 
  895. * @param string $before (default: ''). 
  896. * @param string $after (default: ''). 
  897. * @return string 
  898. */ 
  899. function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '' ) { 
  900. return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after ); 
  901.  
  902. /** 
  903. * Returns the product tags in a list. 
  904. * 
  905. * @param int $product_id 
  906. * @param string $sep (default: ', '). 
  907. * @param string $before (default: ''). 
  908. * @param string $after (default: ''). 
  909. * @return string 
  910. */ 
  911. function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) { 
  912. return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after ); 
  913.  
  914. /** 
  915. * Callback for array filter to get visible only. 
  916. * @since 3.0.0 
  917. * @param WC_Product $product 
  918. * @return bool 
  919. */ 
  920. function wc_products_array_filter_visible( $product ) { 
  921. return $product && is_a( $product, 'WC_Product' ) && $product->is_visible(); 
  922.  
  923. /** 
  924. * Callback for array filter to get products the user can edit only. 
  925. * 
  926. * @since 3.0.0 
  927. * @param WC_Product $product 
  928. * @return bool 
  929. */ 
  930. function wc_products_array_filter_editable( $product ) { 
  931. return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() ); 
  932.  
  933. /** 
  934. * Sort an array of products by a value. 
  935. * @since 3.0.0 
  936. * @param array $products 
  937. * @param string $orderby 
  938. * @return array 
  939. */ 
  940. function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) { 
  941. $orderby = strtolower( $orderby ); 
  942. $order = strtolower( $order ); 
  943. switch ( $orderby ) { 
  944. case 'title' : 
  945. case 'id' : 
  946. case 'date' : 
  947. case 'modified' : 
  948. case 'menu_order' : 
  949. case 'price' : 
  950. usort( $products, 'wc_products_array_orderby_' . $orderby ); 
  951. break; 
  952. default : 
  953. shuffle( $products ); 
  954. break; 
  955. if ( 'desc' === $order ) { 
  956. $products = array_reverse( $products ); 
  957. return $products; 
  958.  
  959. /** 
  960. * Sort by title. 
  961. * @since 3.0.0 
  962. * @param WC_Product object $a 
  963. * @param WC_Product object $b 
  964. * @return int 
  965. */ 
  966. function wc_products_array_orderby_title( $a, $b ) { 
  967. return strcasecmp( $a->get_name(), $b->get_name() ); 
  968.  
  969. /** 
  970. * Sort by id. 
  971. * @since 3.0.0 
  972. * @param WC_Product object $a 
  973. * @param WC_Product object $b 
  974. * @return int 
  975. */ 
  976. function wc_products_array_orderby_id( $a, $b ) { 
  977. if ( $a->get_id() === $b->get_id() ) { 
  978. return 0; 
  979. return ( $a->get_id() < $b->get_id() ) ? -1 : 1; 
  980.  
  981. /** 
  982. * Sort by date. 
  983. * @since 3.0.0 
  984. * @param WC_Product object $a 
  985. * @param WC_Product object $b 
  986. * @return int 
  987. */ 
  988. function wc_products_array_orderby_date( $a, $b ) { 
  989. if ( $a->get_date_created() === $b->get_date_created() ) { 
  990. return 0; 
  991. return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1; 
  992.  
  993. /** 
  994. * Sort by modified. 
  995. * @since 3.0.0 
  996. * @param WC_Product object $a 
  997. * @param WC_Product object $b 
  998. * @return int 
  999. */ 
  1000. function wc_products_array_orderby_modified( $a, $b ) { 
  1001. if ( $a->get_date_modified() === $b->get_date_modified() ) { 
  1002. return 0; 
  1003. return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1; 
  1004.  
  1005. /** 
  1006. * Sort by menu order. 
  1007. * @since 3.0.0 
  1008. * @param WC_Product object $a 
  1009. * @param WC_Product object $b 
  1010. * @return int 
  1011. */ 
  1012. function wc_products_array_orderby_menu_order( $a, $b ) { 
  1013. if ( $a->get_menu_order() === $b->get_menu_order() ) { 
  1014. return 0; 
  1015. return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1; 
  1016.  
  1017. /** 
  1018. * Sort by price low to high. 
  1019. * @since 3.0.0 
  1020. * @param WC_Product object $a 
  1021. * @param WC_Product object $b 
  1022. * @return int 
  1023. */ 
  1024. function wc_products_array_orderby_price( $a, $b ) { 
  1025. if ( $a->get_price() === $b->get_price() ) { 
  1026. return 0; 
  1027. return ( $a->get_price() < $b->get_price() ) ? -1 : 1; 
  1028.  
  1029. /** 
  1030. * Queue a product for syncing at the end of the request. 
  1031. * 
  1032. * @param int $product_id 
  1033. */ 
  1034. function wc_deferred_product_sync( $product_id ) { 
  1035. global $wc_deferred_product_sync; 
  1036.  
  1037. if ( empty( $wc_deferred_product_sync ) ) { 
  1038. $wc_deferred_product_sync = array(); 
  1039.  
  1040. $wc_deferred_product_sync[] = $product_id; 
.