2013-08-09 16:11:15 +00:00
< ? php
/**
* WooCommerce Terms
*
* Functions for handling terms / term meta .
*
* @ author WooThemes
* @ category Core
* @ package WooCommerce / Functions
* @ version 2.1 . 0
*/
if ( ! defined ( 'ABSPATH' ) ) exit ; // Exit if accessed directly
2013-09-13 11:07:54 +00:00
/**
2014-02-07 11:43:34 +00:00
* Wrapper for wp_get_post_terms which supports ordering by parent .
*
* NOTE : At this point in time , ordering by menu_order for example isn ' t possible with this function . wp_get_post_terms has no
* filters which we can utilise to modify it ' s query . https :// core . trac . wordpress . org / ticket / 19094
*
2013-11-25 13:30:20 +00:00
* @ param int $product_id
* @ param string $taxonomy
* @ param array $args
* @ return array
2013-09-13 11:07:54 +00:00
*/
function wc_get_product_terms ( $product_id , $taxonomy , $args = array () ) {
2013-11-25 13:30:20 +00:00
if ( ! taxonomy_exists ( $taxonomy ) )
return array ();
2014-02-07 11:38:57 +00:00
if ( empty ( $args [ 'orderby' ] ) && taxonomy_is_product_attribute ( $taxonomy ) ) {
$args [ 'orderby' ] = wc_attribute_orderby ( $taxonomy );
2013-11-25 13:30:20 +00:00
}
2014-02-07 11:38:57 +00:00
// Support ordering by parent
2013-11-25 13:30:20 +00:00
if ( ! empty ( $args [ 'orderby' ] ) && $args [ 'orderby' ] == 'parent' ) {
2014-02-12 11:47:56 +00:00
$fields = isset ( $args [ 'fields' ] ) ? $args [ 'fields' ] : 'all' ;
2013-11-25 13:30:20 +00:00
2014-02-12 11:47:56 +00:00
// Unset for wp_get_post_terms
2013-11-25 13:30:20 +00:00
unset ( $args [ 'orderby' ] );
unset ( $args [ 'fields' ] );
2013-09-13 11:07:54 +00:00
2014-02-12 11:47:56 +00:00
$terms = wp_get_post_terms ( $product_id , $taxonomy , $args );
2013-09-13 11:07:54 +00:00
usort ( $terms , '_wc_get_product_terms_parent_usort_callback' );
2013-11-25 13:30:20 +00:00
switch ( $fields ) {
case 'names' :
$terms = wp_list_pluck ( $terms , 'name' );
2014-02-12 11:47:56 +00:00
break ;
2013-11-25 13:30:20 +00:00
case 'ids' :
$terms = wp_list_pluck ( $terms , 'term_id' );
2014-02-12 11:47:56 +00:00
break ;
2013-11-25 13:30:20 +00:00
case 'slugs' :
$terms = wp_list_pluck ( $terms , 'slug' );
2014-02-12 11:47:56 +00:00
break ;
2013-11-25 13:30:20 +00:00
}
2014-02-12 11:47:56 +00:00
} else {
$terms = wp_get_post_terms ( $product_id , $taxonomy , $args );
2013-11-25 13:30:20 +00:00
}
2013-09-13 11:07:54 +00:00
return $terms ;
}
/**
* Sort by parent
2013-11-25 13:30:20 +00:00
* @ param WP_POST object $a
* @ param WP_POST object $b
* @ return int
2013-09-13 11:07:54 +00:00
*/
function _wc_get_product_terms_parent_usort_callback ( $a , $b ) {
if ( $a -> parent === $b -> parent )
return 0 ;
return ( $a -> parent < $b -> parent ) ? 1 : - 1 ;
}
2013-08-09 16:11:15 +00:00
/**
* WooCommerce Dropdown categories
*
* Stuck with this until a fix for http :// core . trac . wordpress . org / ticket / 13258
* We use a custom walker , just like WordPress does
*
* @ param int $show_counts ( default : 1 )
* @ param int $hierarchical ( default : 1 )
* @ param int $show_uncategorized ( default : 1 )
* @ return string
*/
2013-11-25 13:30:20 +00:00
function wc_product_dropdown_categories ( $args = array (), $deprecated_hierarchical = 1 , $deprecated_show_uncategorized = 1 , $deprecated_orderby = '' ) {
2013-08-09 16:11:15 +00:00
global $wp_query , $woocommerce ;
2013-08-20 12:32:38 +00:00
if ( ! is_array ( $args ) ) {
2013-11-25 13:30:20 +00:00
_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.' );
2013-08-20 12:32:38 +00:00
$args [ 'show_counts' ] = $args ;
$args [ 'hierarchical' ] = $deprecated_hierarchical ;
$args [ 'show_uncategorized' ] = $deprecated_show_uncategorized ;
$args [ 'orderby' ] = $deprecated_orderby ;
}
$defaults = array (
'pad_counts' => 1 ,
'show_counts' => 1 ,
'hierarchical' => 1 ,
'hide_empty' => 1 ,
'show_uncategorized' => 1 ,
'orderby' => 'name' ,
'selected' => isset ( $wp_query -> query [ 'product_cat' ] ) ? $wp_query -> query [ 'product_cat' ] : '' ,
'menu_order' => false
);
$args = wp_parse_args ( $args , $defaults );
2013-08-09 16:11:15 +00:00
2014-02-28 15:27:20 +00:00
if ( $args [ 'orderby' ] == 'order' ) {
$args [ 'menu_order' ] = 'asc' ;
$args [ 'orderby' ] = 'name' ;
}
2013-08-09 16:11:15 +00:00
2013-08-20 12:32:38 +00:00
$terms = get_terms ( 'product_cat' , $args );
2013-08-09 16:11:15 +00:00
if ( ! $terms )
return ;
$output = " <select name='product_cat' id='dropdown_product_cat'> " ;
2013-08-20 12:32:38 +00:00
$output .= '<option value="" ' . selected ( isset ( $_GET [ 'product_cat' ] ) ? $_GET [ 'product_cat' ] : '' , '' , false ) . '>' . __ ( 'Select a category' , 'woocommerce' ) . '</option>' ;
2013-11-25 13:30:20 +00:00
$output .= wc_walk_category_dropdown_tree ( $terms , 0 , $args );
2013-08-09 16:11:15 +00:00
2013-08-20 12:32:38 +00:00
if ( $args [ 'show_uncategorized' ] )
2013-08-09 16:11:15 +00:00
$output .= '<option value="0" ' . selected ( isset ( $_GET [ 'product_cat' ] ) ? $_GET [ 'product_cat' ] : '' , '0' , false ) . '>' . __ ( 'Uncategorized' , 'woocommerce' ) . '</option>' ;
$output .= " </select> " ;
echo $output ;
}
/**
* Walk the Product Categories .
*
2013-11-27 18:20:31 +00:00
* @ return mixed
2013-08-09 16:11:15 +00:00
*/
2013-11-25 13:30:20 +00:00
function wc_walk_category_dropdown_tree () {
2013-08-09 16:11:15 +00:00
global $woocommerce ;
if ( ! class_exists ( 'WC_Product_Cat_Dropdown_Walker' ) )
2013-11-25 14:01:32 +00:00
include_once ( WC () -> plugin_path () . '/includes/walkers/class-product-cat-dropdown-walker.php' );
2013-08-09 16:11:15 +00:00
$args = func_get_args ();
// the user's options are the third parameter
if ( empty ( $args [ 2 ][ 'walker' ]) || ! is_a ( $args [ 2 ][ 'walker' ], 'Walker' ) )
$walker = new WC_Product_Cat_Dropdown_Walker ;
else
$walker = $args [ 2 ][ 'walker' ];
return call_user_func_array ( array ( & $walker , 'walk' ), $args );
}
/**
* WooCommerce Term / Order item Meta API - set table name
*
* @ return void
*/
2013-11-25 13:30:20 +00:00
function wc_taxonomy_metadata_wpdbfix () {
2013-08-09 16:11:15 +00:00
global $wpdb ;
$termmeta_name = 'woocommerce_termmeta' ;
$itemmeta_name = 'woocommerce_order_itemmeta' ;
$wpdb -> woocommerce_termmeta = $wpdb -> prefix . $termmeta_name ;
$wpdb -> order_itemmeta = $wpdb -> prefix . $itemmeta_name ;
$wpdb -> tables [] = 'woocommerce_termmeta' ;
2013-09-26 13:59:53 +00:00
$wpdb -> tables [] = 'woocommerce_order_itemmeta' ;
2013-08-09 16:11:15 +00:00
}
2013-11-25 13:30:20 +00:00
add_action ( 'init' , 'wc_taxonomy_metadata_wpdbfix' , 0 );
add_action ( 'switch_blog' , 'wc_taxonomy_metadata_wpdbfix' , 0 );
2013-08-09 16:11:15 +00:00
/**
* WooCommerce Term Meta API - Update term meta
*
* @ param mixed $term_id
* @ param mixed $meta_key
* @ param mixed $meta_value
* @ param string $prev_value ( default : '' )
* @ return bool
*/
function update_woocommerce_term_meta ( $term_id , $meta_key , $meta_value , $prev_value = '' ) {
return update_metadata ( 'woocommerce_term' , $term_id , $meta_key , $meta_value , $prev_value );
}
/**
* WooCommerce Term Meta API - Add term meta
*
* @ param mixed $term_id
* @ param mixed $meta_key
* @ param mixed $meta_value
* @ param bool $unique ( default : false )
* @ return bool
*/
function add_woocommerce_term_meta ( $term_id , $meta_key , $meta_value , $unique = false ){
return add_metadata ( 'woocommerce_term' , $term_id , $meta_key , $meta_value , $unique );
}
/**
* WooCommerce Term Meta API - Delete term meta
*
* @ param mixed $term_id
* @ param mixed $meta_key
* @ param string $meta_value ( default : '' )
* @ param bool $delete_all ( default : false )
* @ return bool
*/
function delete_woocommerce_term_meta ( $term_id , $meta_key , $meta_value = '' , $delete_all = false ) {
return delete_metadata ( 'woocommerce_term' , $term_id , $meta_key , $meta_value , $delete_all );
}
/**
* WooCommerce Term Meta API - Get term meta
*
* @ param mixed $term_id
* @ param mixed $key
* @ param bool $single ( default : true )
* @ return mixed
*/
function get_woocommerce_term_meta ( $term_id , $key , $single = true ) {
return get_metadata ( 'woocommerce_term' , $term_id , $key , $single );
}
/**
* Move a term before the a given element of its hierarchy level
*
* @ param int $the_term
* @ param int $next_id the id of the next sibling element in save hierarchy level
* @ param string $taxonomy
* @ param int $index ( default : 0 )
* @ param mixed $terms ( default : null )
* @ return int
*/
2013-11-25 13:30:20 +00:00
function wc_reorder_terms ( $the_term , $next_id , $taxonomy , $index = 0 , $terms = null ) {
2013-08-09 16:11:15 +00:00
if ( ! $terms ) $terms = get_terms ( $taxonomy , 'menu_order=ASC&hide_empty=0&parent=0' );
if ( empty ( $terms ) ) return $index ;
$id = $the_term -> term_id ;
$term_in_level = false ; // flag: is our term to order in this level of terms
foreach ( $terms as $term ) {
if ( $term -> term_id == $id ) { // our term to order, we skip
$term_in_level = true ;
continue ; // our term to order, we skip
}
// the nextid of our term to order, lets move our term here
if ( null !== $next_id && $term -> term_id == $next_id ) {
$index ++ ;
2013-11-25 13:30:20 +00:00
$index = wc_set_term_order ( $id , $index , $taxonomy , true );
2013-08-09 16:11:15 +00:00
}
// set order
$index ++ ;
2013-11-25 13:30:20 +00:00
$index = wc_set_term_order ( $term -> term_id , $index , $taxonomy );
2013-08-09 16:11:15 +00:00
// if that term has children we walk through them
$children = get_terms ( $taxonomy , " parent= { $term -> term_id } &menu_order=ASC&hide_empty=0 " );
if ( ! empty ( $children ) ) {
2013-12-05 16:07:44 +00:00
$index = wc_reorder_terms ( $the_term , $next_id , $taxonomy , $index , $children );
2013-08-09 16:11:15 +00:00
}
}
// no nextid meaning our term is in last position
if ( $term_in_level && null === $next_id )
2013-11-25 13:30:20 +00:00
$index = wc_set_term_order ( $id , $index + 1 , $taxonomy , true );
2013-08-09 16:11:15 +00:00
return $index ;
}
/**
* Set the sort order of a term
*
* @ param int $term_id
* @ param int $index
* @ param string $taxonomy
* @ param bool $recursive ( default : false )
* @ return int
*/
2013-11-25 13:30:20 +00:00
function wc_set_term_order ( $term_id , $index , $taxonomy , $recursive = false ) {
2013-08-09 16:11:15 +00:00
$term_id = ( int ) $term_id ;
$index = ( int ) $index ;
// Meta name
if ( taxonomy_is_product_attribute ( $taxonomy ) )
$meta_name = 'order_' . esc_attr ( $taxonomy );
else
$meta_name = 'order' ;
update_woocommerce_term_meta ( $term_id , $meta_name , $index );
if ( ! $recursive ) return $index ;
$children = get_terms ( $taxonomy , " parent= $term_id &menu_order=ASC&hide_empty=0 " );
foreach ( $children as $term ) {
$index ++ ;
2013-11-25 13:30:20 +00:00
$index = wc_set_term_order ( $term -> term_id , $index , $taxonomy , true );
2013-08-09 16:11:15 +00:00
}
clean_term_cache ( $term_id , $taxonomy );
return $index ;
}
/**
* Add term ordering to get_terms
*
* It enables the support a 'menu_order' parameter to get_terms for the product_cat taxonomy .
* By default it is 'ASC' . It accepts 'DESC' too
*
* To disable it , set it ot false ( or 0 )
*
* @ param array $clauses
* @ param array $taxonomies
* @ param array $args
* @ return array
*/
2013-11-25 13:30:20 +00:00
function wc_terms_clauses ( $clauses , $taxonomies , $args ) {
2013-08-09 16:11:15 +00:00
global $wpdb , $woocommerce ;
// No sorting when menu_order is false
2014-02-07 11:38:57 +00:00
if ( isset ( $args [ 'menu_order' ] ) && $args [ 'menu_order' ] == false ) {
return $clauses ;
}
2013-08-09 16:11:15 +00:00
// No sorting when orderby is non default
2014-02-07 11:38:57 +00:00
if ( isset ( $args [ 'orderby' ] ) && $args [ 'orderby' ] != 'name' ) {
return $clauses ;
}
2013-08-09 16:11:15 +00:00
// No sorting in admin when sorting by a column
2014-02-07 11:38:57 +00:00
if ( is_admin () && isset ( $_GET [ 'orderby' ] ) ) {
return $clauses ;
}
2013-08-09 16:11:15 +00:00
// wordpress should give us the taxonomies asked when calling the get_terms function. Only apply to categories and pa_ attributes
$found = false ;
foreach ( ( array ) $taxonomies as $taxonomy ) {
if ( taxonomy_is_product_attribute ( $taxonomy ) || in_array ( $taxonomy , apply_filters ( 'woocommerce_sortable_taxonomies' , array ( 'product_cat' ) ) ) ) {
$found = true ;
break ;
}
}
2014-02-07 11:38:57 +00:00
if ( ! $found ) {
return $clauses ;
}
2013-08-09 16:11:15 +00:00
// Meta name
if ( ! empty ( $taxonomies [ 0 ] ) && taxonomy_is_product_attribute ( $taxonomies [ 0 ] ) ) {
$meta_name = 'order_' . esc_attr ( $taxonomies [ 0 ] );
} else {
$meta_name = 'order' ;
}
// query fields
2014-02-07 11:38:57 +00:00
if ( strpos ( 'COUNT(*)' , $clauses [ 'fields' ] ) === false ) {
$clauses [ 'fields' ] .= ', tm.* ' ;
}
2013-08-09 16:11:15 +00:00
//query join
$clauses [ 'join' ] .= " LEFT JOIN { $wpdb -> woocommerce_termmeta } AS tm ON (t.term_id = tm.woocommerce_term_id AND tm.meta_key = ' " . $meta_name . " ') " ;
// default to ASC
2014-02-07 11:38:57 +00:00
if ( ! isset ( $args [ 'menu_order' ] ) || ! in_array ( strtoupper ( $args [ 'menu_order' ]), array ( 'ASC' , 'DESC' )) ) {
$args [ 'menu_order' ] = 'ASC' ;
}
2013-08-09 16:11:15 +00:00
$order = " ORDER BY tm.meta_value+0 " . $args [ 'menu_order' ];
if ( $clauses [ 'orderby' ] ) :
$clauses [ 'orderby' ] = str_replace ( 'ORDER BY' , $order . ',' , $clauses [ 'orderby' ] );
else :
$clauses [ 'orderby' ] = $order ;
endif ;
return $clauses ;
}
2013-11-25 13:30:20 +00:00
add_filter ( 'terms_clauses' , 'wc_terms_clauses' , 10 , 3 );
2013-08-09 16:11:15 +00:00
/**
* Function for recounting product terms , ignoring hidden products .
2013-11-25 13:30:20 +00:00
* @ param array $terms
* @ param string $taxonomy
* @ param boolean $callback
* @ param boolean $terms_are_term_taxonomy_ids
2013-08-09 16:11:15 +00:00
* @ return void
*/
2013-11-25 13:30:20 +00:00
function _wc_term_recount ( $terms , $taxonomy , $callback = true , $terms_are_term_taxonomy_ids = true ) {
2013-08-09 16:11:15 +00:00
global $wpdb ;
// Standard callback
if ( $callback )
_update_post_term_count ( $terms , $taxonomy );
// Stock query
if ( get_option ( 'woocommerce_hide_out_of_stock_items' ) == 'yes' ) {
$stock_join = " LEFT JOIN { $wpdb -> postmeta } AS meta_stock ON posts.ID = meta_stock.post_id " ;
$stock_query = "
2013-10-22 15:20:40 +00:00
AND meta_stock . meta_key = '_stock_status'
AND meta_stock . meta_value = 'instock'
" ;
2013-08-09 16:11:15 +00:00
} else {
$stock_query = $stock_join = '' ;
}
// Main query
2013-10-22 15:20:40 +00:00
$count_query = "
2013-08-09 16:11:15 +00:00
SELECT COUNT ( DISTINCT posts . ID ) FROM { $wpdb -> posts } as posts
LEFT JOIN { $wpdb -> postmeta } AS meta_visibility ON posts . ID = meta_visibility . post_id
2014-02-13 14:51:43 +00:00
LEFT JOIN { $wpdb -> term_relationships } AS rel ON posts . ID = rel . object_ID
LEFT JOIN { $wpdb -> term_taxonomy } AS tax USING ( term_taxonomy_id )
LEFT JOIN { $wpdb -> terms } AS term USING ( term_id )
LEFT JOIN { $wpdb -> postmeta } AS postmeta ON posts . ID = postmeta . post_id
2013-08-09 16:11:15 +00:00
$stock_join
2013-10-22 15:20:40 +00:00
WHERE post_status = 'publish'
AND post_type = 'product'
AND meta_visibility . meta_key = '_visibility'
AND meta_visibility . meta_value IN ( 'visible' , 'catalog' )
2013-08-09 16:11:15 +00:00
$stock_query
2013-10-22 15:20:40 +00:00
" ;
2013-08-09 16:11:15 +00:00
// Pre-process term taxonomy ids
2013-10-22 15:20:40 +00:00
if ( ! $terms_are_term_taxonomy_ids )
$terms = ( array ) array_keys ( $terms );
2013-08-09 16:11:15 +00:00
2013-10-22 15:20:40 +00:00
$terms = array_filter ( ( array ) $terms );
2013-08-09 16:11:15 +00:00
2013-10-22 15:20:40 +00:00
// Ancestors need counting
2013-10-29 11:29:17 +00:00
if ( is_taxonomy_hierarchical ( $taxonomy -> name ) && $terms ) {
2013-10-22 15:20:40 +00:00
foreach ( $terms as $term_id ) {
2013-10-29 11:29:17 +00:00
$terms = array_merge ( $terms , get_ancestors ( $term_id , $taxonomy -> name ) );
2013-08-09 16:11:15 +00:00
}
}
2013-10-22 15:20:40 +00:00
// Unique terms
$terms = array_unique ( $terms );
2013-08-09 16:11:15 +00:00
2013-10-22 15:20:40 +00:00
// Count the terms
if ( $terms ) {
foreach ( $terms as $term_id ) {
$terms_to_count = array ( absint ( $term_id ) );
2013-08-09 16:11:15 +00:00
2013-10-22 15:20:40 +00:00
if ( is_taxonomy_hierarchical ( $taxonomy -> name ) ) {
// We need to get the $term's hierarchy so we can count its children too
if ( ( $children = get_term_children ( $term_id , $taxonomy -> name ) ) && ! is_wp_error ( $children ) )
$terms_to_count = array_unique ( array_map ( 'absint' , array_merge ( $terms_to_count , $children ) ) );
}
2013-08-09 16:11:15 +00:00
// Generate term query
2014-02-13 14:51:43 +00:00
$term_query = 'AND term_id IN ( ' . implode ( ',' , $terms_to_count ) . ' )' ;
2013-08-09 16:11:15 +00:00
// Get the count
$count = $wpdb -> get_var ( $count_query . $term_query );
2013-10-22 15:20:40 +00:00
// Update the count
update_woocommerce_term_meta ( $term_id , 'product_count_' . $taxonomy -> name , absint ( $count ) );
2013-08-09 16:11:15 +00:00
}
}
}
/**
2013-11-25 13:30:20 +00:00
* Recount terms after the stock amount changes
* @ param int $product_id
2013-08-09 16:11:15 +00:00
* @ return void
*/
2013-11-25 13:30:20 +00:00
function wc_recount_after_stock_change ( $product_id ) {
2013-10-22 15:20:40 +00:00
if ( get_option ( 'woocommerce_hide_out_of_stock_items' ) != 'yes' )
return ;
2013-08-09 16:11:15 +00:00
$product_terms = get_the_terms ( $product_id , 'product_cat' );
if ( $product_terms ) {
foreach ( $product_terms as $term )
$product_cats [ $term -> term_id ] = $term -> parent ;
2013-11-25 13:30:20 +00:00
_wc_term_recount ( $product_cats , get_taxonomy ( 'product_cat' ), false , false );
2013-08-09 16:11:15 +00:00
}
$product_terms = get_the_terms ( $product_id , 'product_tag' );
if ( $product_terms ) {
foreach ( $product_terms as $term )
$product_tags [ $term -> term_id ] = $term -> parent ;
2013-11-25 13:30:20 +00:00
_wc_term_recount ( $product_tags , get_taxonomy ( 'product_tag' ), false , false );
2013-08-09 16:11:15 +00:00
}
}
2013-11-25 13:30:20 +00:00
add_action ( 'woocommerce_product_set_stock_status' , 'wc_recount_after_stock_change' );
2013-08-09 16:11:15 +00:00
/**
* Overrides the original term count for product categories and tags with the product count
* that takes catalog visibility into account .
*
* @ param array $terms
* @ param mixed $taxonomies
* @ param mixed $args
* @ return array
*/
2013-11-25 13:30:20 +00:00
function wc_change_term_counts ( $terms , $taxonomies , $args ) {
2013-08-09 16:11:15 +00:00
if ( is_admin () || is_ajax () )
return $terms ;
2013-10-14 19:20:47 +00:00
if ( ! isset ( $taxonomies [ 0 ] ) || ! in_array ( $taxonomies [ 0 ], apply_filters ( 'woocommerce_change_term_counts' , array ( 'product_cat' , 'product_tag' ) ) ) )
2013-08-09 16:11:15 +00:00
return $terms ;
$term_counts = $o_term_counts = get_transient ( 'wc_term_counts' );
foreach ( $terms as & $term ) {
// If the original term count is zero, there's no way the product count could be higher.
if ( empty ( $term -> count ) ) continue ;
$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 );
if ( $term_counts [ $term -> term_id ] != '' )
$term -> count = $term_counts [ $term -> term_id ];
}
// Update transient
if ( $term_counts != $o_term_counts )
2014-03-07 08:29:01 +00:00
set_transient ( 'wc_term_counts' , $term_counts , DAY_IN_SECONDS );
2013-08-09 16:11:15 +00:00
return $terms ;
}
2013-11-25 13:30:20 +00:00
add_filter ( 'get_terms' , 'wc_change_term_counts' , 10 , 3 );