1 <?php
  2 /**
  3  * WooCommerce Terms
  4  *
  5  * Functions for handling terms/term meta.
  6  *
  7  * @author      WooThemes
  8  * @category    Core
  9  * @package     WooCommerce/Functions
 10  * @version     2.1.0
 11  */
 12 
 13 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
 14 
 15 /**
 16  * Wrapper for wp_get_post_terms which supports ordering by parent.
 17  *
 18  * NOTE: At this point in time, ordering by menu_order for example isn't possible with this function. wp_get_post_terms has no
 19  *   filters which we can utilise to modify it's query. https://core.trac.wordpress.org/ticket/19094
 20  * 
 21  * @param  int $product_id
 22  * @param  string $taxonomy
 23  * @param  array  $args
 24  * @return array
 25  */
 26 function wc_get_product_terms( $product_id, $taxonomy, $args = array() ) {
 27     if ( ! taxonomy_exists( $taxonomy ) ) {
 28         return array();
 29     }
 30 
 31     if ( empty( $args['orderby'] ) && taxonomy_is_product_attribute( $taxonomy ) ) {
 32         $args['orderby'] = wc_attribute_orderby( $taxonomy );
 33     }
 34 
 35     // Support ordering by parent
 36     if ( ! empty( $args['orderby'] ) && $args['orderby'] === 'parent' ) {
 37         $fields = isset( $args['fields'] ) ? $args['fields'] : 'all';
 38 
 39         // Unset for wp_get_post_terms
 40         unset( $args['orderby'] );
 41         unset( $args['fields'] );
 42 
 43         $terms = wp_get_post_terms( $product_id, $taxonomy, $args );
 44 
 45         usort( $terms, '_wc_get_product_terms_parent_usort_callback' );
 46 
 47         switch ( $fields ) {
 48             case 'names' :
 49                 $terms = wp_list_pluck( $terms, 'name' );
 50                 break;
 51             case 'ids' :
 52                 $terms = wp_list_pluck( $terms, 'term_id' );
 53                 break;
 54             case 'slugs' :
 55                 $terms = wp_list_pluck( $terms, 'slug' );
 56                 break;
 57         }
 58     } elseif ( ! empty( $args['orderby'] ) && $args['orderby'] === 'menu_order' ) {
 59         // wp_get_post_terms doesn't let us use custom sort order
 60         $args['include'] = wp_get_post_terms( $product_id, $taxonomy, array( 'fields' => 'ids' ) );
 61         
 62         if ( empty( $args['include'] ) ) {
 63             $terms = array();
 64         } else {
 65             // This isn't needed for get_terms
 66             unset( $args['orderby'] );
 67 
 68             // Set args for get_terms
 69             $args['menu_order'] = isset( $args['order'] ) ? $args['order'] : 'ASC';
 70             $args['hide_empty'] = isset( $args['hide_empty'] ) ? $args['hide_empty'] : 0;
 71             $args['fields']     = isset( $args['fields'] ) ? $args['fields'] : 'names';
 72 
 73             // Ensure slugs is valid for get_terms - slugs isn't supported
 74             $args['fields']     = $args['fields'] === 'slugs' ? 'id=>slug' : $args['fields'];
 75             $terms              = get_terms( $taxonomy, $args );
 76         }
 77     } else {
 78         $terms = wp_get_post_terms( $product_id, $taxonomy, $args );
 79     }
 80 
 81     return $terms;
 82 }
 83 
 84 /**
 85  * Sort by parent
 86  * @param  WP_POST object $a
 87  * @param  WP_POST object $b
 88  * @return int
 89  */
 90 function _wc_get_product_terms_parent_usort_callback( $a, $b ) {
 91     if ( $a->parent === $b->parent ) {
 92         return 0;
 93     }
 94     return ( $a->parent < $b->parent ) ? 1 : -1;
 95 }
 96 
 97 /**
 98  * WooCommerce Dropdown categories
 99  *
100  * Stuck with this until a fix for http://core.trac.wordpress.org/ticket/13258
101  * We use a custom walker, just like WordPress does
102  *
103  * @param int $show_counts (default: 1)
104  * @param int $hierarchical (default: 1)
105  * @param int $show_uncategorized (default: 1)
106  * @return string
107  */
108 function wc_product_dropdown_categories( $args = array(), $deprecated_hierarchical = 1, $deprecated_show_uncategorized = 1, $deprecated_orderby = '' ) {
109     global $wp_query, $woocommerce;
110 
111     if ( ! is_array( $args ) ) {
112         _deprecated_argument( 'wc_product_dropdown_categories()', '2.1', 'show_counts, hierarchical, show_uncategorized and orderby arguments are invalid - pass a single array of values instead.' );
113 
114         $args['show_counts']        = $args;
115         $args['hierarchical']       = $deprecated_hierarchical;
116         $args['show_uncategorized'] = $deprecated_show_uncategorized;
117         $args['orderby']            = $deprecated_orderby;
118     }
119 
120     $defaults = array(
121         'pad_counts'         => 1,
122         'show_counts'        => 1,
123         'hierarchical'       => 1,
124         'hide_empty'         => 1,
125         'show_uncategorized' => 1,
126         'orderby'            => 'name',
127         'selected'           => isset( $wp_query->query['product_cat'] ) ? $wp_query->query['product_cat'] : '',
128         'menu_order'         => false
129     );
130 
131     $args = wp_parse_args( $args, $defaults );
132 
133     if ( $args['orderby'] == 'order' ) {
134         $args['menu_order'] = 'asc';
135         $args['orderby']    = 'name';
136     }
137 
138     $terms = get_terms( 'product_cat', $args );
139 
140     if ( ! $terms )
141         return;
142 
143     $output  = "<select name='product_cat' id='dropdown_product_cat'>";
144     $output .= '<option value="" ' .  selected( isset( $_GET['product_cat'] ) ? $_GET['product_cat'] : '', '', false ) . '>' . __( 'Select a category', 'woocommerce' ) . '</option>';
145     $output .= wc_walk_category_dropdown_tree( $terms, 0, $args );
146 
147     if ( $args['show_uncategorized'] )
148         $output .= '<option value="0" ' . selected( isset( $_GET['product_cat'] ) ? $_GET['product_cat'] : '', '0', false ) . '>' . __( 'Uncategorized', 'woocommerce' ) . '</option>';
149 
150     $output .="</select>";
151 
152     echo $output;
153 }
154 
155 /**
156  * Walk the Product Categories.
157  *
158  * @return mixed
159  */
160 function wc_walk_category_dropdown_tree() {
161     global $woocommerce;
162 
163     if ( ! class_exists( 'WC_Product_Cat_Dropdown_Walker' ) )
164         include_once( WC()->plugin_path() . '/includes/walkers/class-product-cat-dropdown-walker.php' );
165 
166     $args = func_get_args();
167 
168     // the user's options are the third parameter
169     if ( empty( $args[2]['walker']) || !is_a($args[2]['walker'], 'Walker' ) )
170         $walker = new WC_Product_Cat_Dropdown_Walker;
171     else
172         $walker = $args[2]['walker'];
173 
174     return call_user_func_array(array( &$walker, 'walk' ), $args );
175 }
176 
177 /**
178  * WooCommerce Term/Order item Meta API - set table name
179  *
180  * @return void
181  */
182 function wc_taxonomy_metadata_wpdbfix() {
183     global $wpdb;
184     $termmeta_name = 'woocommerce_termmeta';
185     $itemmeta_name = 'woocommerce_order_itemmeta';
186 
187     $wpdb->woocommerce_termmeta = $wpdb->prefix . $termmeta_name;
188     $wpdb->order_itemmeta = $wpdb->prefix . $itemmeta_name;
189 
190     $wpdb->tables[] = 'woocommerce_termmeta';
191     $wpdb->tables[] = 'woocommerce_order_itemmeta';
192 }
193 add_action( 'init', 'wc_taxonomy_metadata_wpdbfix', 0 );
194 add_action( 'switch_blog', 'wc_taxonomy_metadata_wpdbfix', 0 );
195 
196 /**
197  * WooCommerce Term Meta API - Update term meta
198  *
199  * @param mixed $term_id
200  * @param mixed $meta_key
201  * @param mixed $meta_value
202  * @param string $prev_value (default: '')
203  * @return bool
204  */
205 function update_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) {
206     return update_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $prev_value );
207 }
208 
209 /**
210  * WooCommerce Term Meta API - Add term meta
211  *
212  * @param mixed $term_id
213  * @param mixed $meta_key
214  * @param mixed $meta_value
215  * @param bool $unique (default: false)
216  * @return bool
217  */
218 function add_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $unique = false ){
219     return add_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $unique );
220 }
221 
222 /**
223  * WooCommerce Term Meta API - Delete term meta
224  *
225  * @param mixed $term_id
226  * @param mixed $meta_key
227  * @param string $meta_value (default: '')
228  * @param bool $delete_all (default: false)
229  * @return bool
230  */
231 function delete_woocommerce_term_meta( $term_id, $meta_key, $meta_value = '', $delete_all = false ) {
232     return delete_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $delete_all );
233 }
234 
235 /**
236  * WooCommerce Term Meta API - Get term meta
237  *
238  * @param mixed $term_id
239  * @param mixed $key
240  * @param bool $single (default: true)
241  * @return mixed
242  */
243 function get_woocommerce_term_meta( $term_id, $key, $single = true ) {
244     return get_metadata( 'woocommerce_term', $term_id, $key, $single );
245 }
246 
247 /**
248  * Move a term before the a given element of its hierarchy level
249  *
250  * @param int $the_term
251  * @param int $next_id the id of the next sibling element in save hierarchy level
252  * @param string $taxonomy
253  * @param int $index (default: 0)
254  * @param mixed $terms (default: null)
255  * @return int
256  */
257 function wc_reorder_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) {
258 
259     if( ! $terms ) $terms = get_terms($taxonomy, 'menu_order=ASC&hide_empty=0&parent=0' );
260     if( empty( $terms ) ) return $index;
261 
262     $id = $the_term->term_id;
263 
264     $term_in_level = false; // flag: is our term to order in this level of terms
265 
266     foreach ($terms as $term) {
267 
268         if( $term->term_id == $id ) { // our term to order, we skip
269             $term_in_level = true;
270             continue; // our term to order, we skip
271         }
272         // the nextid of our term to order, lets move our term here
273         if(null !== $next_id && $term->term_id == $next_id) {
274             $index++;
275             $index = wc_set_term_order($id, $index, $taxonomy, true);
276         }
277 
278         // set order
279         $index++;
280         $index = wc_set_term_order($term->term_id, $index, $taxonomy);
281 
282         // if that term has children we walk through them
283         $children = get_terms($taxonomy, "parent={$term->term_id}&menu_order=ASC&hide_empty=0");
284         if( !empty($children) ) {
285             $index = wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $children );
286         }
287     }
288 
289     // no nextid meaning our term is in last position
290     if( $term_in_level && null === $next_id )
291         $index = wc_set_term_order($id, $index+1, $taxonomy, true);
292 
293     return $index;
294 }
295 
296 /**
297  * Set the sort order of a term
298  *
299  * @param int $term_id
300  * @param int $index
301  * @param string $taxonomy
302  * @param bool $recursive (default: false)
303  * @return int
304  */
305 function wc_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) {
306 
307     $term_id    = (int) $term_id;
308     $index      = (int) $index;
309 
310     // Meta name
311     if ( taxonomy_is_product_attribute( $taxonomy ) )
312         $meta_name =  'order_' . esc_attr( $taxonomy );
313     else
314         $meta_name = 'order';
315 
316     update_woocommerce_term_meta( $term_id, $meta_name, $index );
317 
318     if( ! $recursive ) return $index;
319 
320     $children = get_terms($taxonomy, "parent=$term_id&menu_order=ASC&hide_empty=0");
321 
322     foreach ( $children as $term ) {
323         $index ++;
324         $index = wc_set_term_order($term->term_id, $index, $taxonomy, true);
325     }
326 
327     clean_term_cache( $term_id, $taxonomy );
328 
329     return $index;
330 }
331 
332 /**
333  * Add term ordering to get_terms
334  *
335  * It enables the support a 'menu_order' parameter to get_terms for the product_cat taxonomy.
336  * By default it is 'ASC'. It accepts 'DESC' too
337  *
338  * To disable it, set it ot false (or 0)
339  *
340  * @param array $clauses
341  * @param array $taxonomies
342  * @param array $args
343  * @return array
344  */
345 function wc_terms_clauses( $clauses, $taxonomies, $args ) {
346     global $wpdb, $woocommerce;
347 
348     // No sorting when menu_order is false
349     if ( isset( $args['menu_order'] ) && $args['menu_order'] == false ) {
350         return $clauses;
351     }
352 
353     // No sorting when orderby is non default
354     if ( isset( $args['orderby'] ) && $args['orderby'] != 'name' ) {
355         return $clauses;
356     }
357 
358     // No sorting in admin when sorting by a column
359     if ( is_admin() && isset( $_GET['orderby'] ) ) {
360         return $clauses;
361     }
362 
363     // wordpress should give us the taxonomies asked when calling the get_terms function. Only apply to categories and pa_ attributes
364     $found = false;
365     foreach ( (array) $taxonomies as $taxonomy ) {
366         if ( taxonomy_is_product_attribute( $taxonomy ) || in_array( $taxonomy, apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ) ) ) {
367             $found = true;
368             break;
369         }
370     }
371     if ( ! $found ) {
372         return $clauses;
373     }
374 
375     // Meta name
376     if ( ! empty( $taxonomies[0] ) && taxonomy_is_product_attribute( $taxonomies[0] ) ) {
377         $meta_name =  'order_' . esc_attr( $taxonomies[0] );
378     } else {
379         $meta_name = 'order';
380     }
381 
382     // query fields
383     if ( strpos( 'COUNT(*)', $clauses['fields'] ) === false )  {
384         $clauses['fields']  .= ', tm.* ';
385     }
386 
387     //query join
388     $clauses['join'] .= " LEFT JOIN {$wpdb->woocommerce_termmeta} AS tm ON (t.term_id = tm.woocommerce_term_id AND tm.meta_key = '". $meta_name ."') ";
389 
390     // default to ASC
391     if ( ! isset( $args['menu_order'] ) || ! in_array( strtoupper($args['menu_order']), array('ASC', 'DESC')) ) {
392         $args['menu_order'] = 'ASC';
393     }
394 
395     $order = "ORDER BY tm.meta_value+0 " . $args['menu_order'];
396 
397     if ( $clauses['orderby'] ):
398         $clauses['orderby'] = str_replace('ORDER BY', $order . ',', $clauses['orderby'] );
399     else:
400         $clauses['orderby'] = $order;
401     endif;
402 
403     return $clauses;
404 }
405 add_filter( 'terms_clauses', 'wc_terms_clauses', 10, 3 );
406 
407 /**
408  * Function for recounting product terms, ignoring hidden products.
409  * @param  array  $terms
410  * @param  string  $taxonomy
411  * @param  boolean $callback
412  * @param  boolean $terms_are_term_taxonomy_ids
413  * @return void
414  */
415 function _wc_term_recount( $terms, $taxonomy, $callback = true, $terms_are_term_taxonomy_ids = true ) {
416     global $wpdb;
417 
418     // Standard callback
419     if ( $callback ) {
420         _update_post_term_count( $terms, $taxonomy );
421     }
422 
423     // Stock query
424     if ( get_option( 'woocommerce_hide_out_of_stock_items' ) == 'yes' ) {
425         $stock_join  = "LEFT JOIN {$wpdb->postmeta} AS meta_stock ON posts.ID = meta_stock.post_id";
426         $stock_query = "
427         AND meta_stock.meta_key = '_stock_status'
428         AND meta_stock.meta_value = 'instock'
429         ";
430     } else {
431         $stock_query = $stock_join = '';
432     }
433 
434     // Main query
435     $count_query = "
436         SELECT COUNT( DISTINCT posts.ID ) FROM {$wpdb->posts} as posts
437         LEFT JOIN {$wpdb->postmeta} AS meta_visibility ON posts.ID = meta_visibility.post_id
438         LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID=rel.object_ID
439         LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id )
440         LEFT JOIN {$wpdb->terms} AS term USING( term_id )
441         LEFT JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id
442         $stock_join
443         WHERE   post_status = 'publish'
444         AND     post_type   = 'product'
445         AND     meta_visibility.meta_key = '_visibility'
446         AND     meta_visibility.meta_value IN ( 'visible', 'catalog' )
447         $stock_query
448     ";
449 
450     // Pre-process term taxonomy ids
451     if ( ! $terms_are_term_taxonomy_ids ) {
452         // We passed in an array of TERMS in format id=>parent
453         $terms = array_filter( (array) array_keys( $terms ) );
454     } else {
455         // If we have term taxonomy IDs we need to get the term ID
456         $term_taxonomy_ids = $terms;
457         $terms             = array();
458         foreach ( $term_taxonomy_ids as $term_taxonomy_id ) {
459             $term    = get_term_by( 'term_taxonomy_id', $term_taxonomy_id, $taxonomy->name );
460             $terms[] = $term->term_id;
461         }
462     }
463 
464     // Exit if we have no terms to count
465     if ( ! $terms ) {
466         return;
467     }
468 
469     // Ancestors need counting
470     if ( is_taxonomy_hierarchical( $taxonomy->name ) ) {
471         foreach ( $terms as $term_id ) {
472             $terms = array_merge( $terms, get_ancestors( $term_id, $taxonomy->name ) );
473         }
474     }
475 
476     // Unique terms only
477     $terms = array_unique( $terms );
478 
479     // Count the terms
480     foreach ( $terms as $term_id ) {
481         $terms_to_count = array( absint( $term_id ) );
482 
483         if ( is_taxonomy_hierarchical( $taxonomy->name ) ) {
484             // We need to get the $term's hierarchy so we can count its children too
485             if ( ( $children = get_term_children( $term_id, $taxonomy->name ) ) && ! is_wp_error( $children ) ) {
486                 $terms_to_count = array_unique( array_map( 'absint', array_merge( $terms_to_count, $children ) ) );
487             }
488         }
489 
490         // Generate term query
491         $term_query = 'AND term_id IN ( ' . implode( ',', $terms_to_count ) . ' )';
492 
493         // Get the count
494         $count = $wpdb->get_var( $count_query . $term_query );
495 
496         // Update the count
497         update_woocommerce_term_meta( $term_id, 'product_count_' . $taxonomy->name, absint( $count ) );
498     }
499 }
500 
501 /**
502  * Recount terms after the stock amount changes
503  * @param  int $product_id 
504  * @return void
505  */
506 function wc_recount_after_stock_change( $product_id ) {
507     if ( get_option( 'woocommerce_hide_out_of_stock_items' ) != 'yes' ) 
508         return;
509 
510     $product_terms = get_the_terms( $product_id, 'product_cat' );
511 
512     if ( $product_terms ) {
513         foreach ( $product_terms as $term )
514             $product_cats[ $term->term_id ] = $term->parent;
515 
516         _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), false, false );
517     }
518 
519     $product_terms = get_the_terms( $product_id, 'product_tag' );
520 
521     if ( $product_terms ) {
522         foreach ( $product_terms as $term )
523             $product_tags[ $term->term_id ] = $term->parent;
524 
525         _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), false, false );
526     }
527 
528     delete_transient( 'wc_term_counts' );
529 }
530 add_action( 'woocommerce_product_set_stock_status', 'wc_recount_after_stock_change' );
531 
532 
533 /**
534  * Overrides the original term count for product categories and tags with the product count
535  * that takes catalog visibility into account.
536  *
537  * @param array $terms
538  * @param mixed $taxonomies
539  * @param mixed $args
540  * @return array
541  */
542 function wc_change_term_counts( $terms, $taxonomies, $args ) {
543     if ( is_admin() || is_ajax() ) {
544         return $terms;
545     }
546 
547     if ( ! isset( $taxonomies[0] ) || ! in_array( $taxonomies[0], apply_filters( 'woocommerce_change_term_counts', array( 'product_cat', 'product_tag' ) ) ) ) {
548         return $terms;
549     }
550 
551     $term_counts = $o_term_counts = get_transient( 'wc_term_counts' );
552 
553     foreach ( $terms as &$term ) {
554         if ( is_object( $term ) ) {
555             $term_counts[ $term->term_id ] = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : get_woocommerce_term_meta( $term->term_id, 'product_count_' . $taxonomies[0] , true );
556 
557             if ( $term_counts[ $term->term_id ] !== '' ) {
558                 $term->count = absint( $term_counts[ $term->term_id ] );
559             }
560         }
561     }
562 
563     // Update transient
564     if ( $term_counts != $o_term_counts ) {
565         set_transient( 'wc_term_counts', $term_counts, YEAR_IN_SECONDS );
566     }
567 
568     return $terms;
569 }
570 add_filter( 'get_terms', 'wc_change_term_counts', 10, 3 );
571 
WooCommerce API documentation generated by ApiGen 2.8.0