/** * Sorts products by price, using their currency-specific prices. * * HOW TO USE THIS CODE * This code shows how to override the "sort by price" logic used by WooCommerce, to ensure that products * are sorted using their currency-specific price. * * DISCLAIMER * THE USE OF THIS CODE IS AT YOUR OWN RISK. You remain fully liable for compliance with tax laws. * This code is offered free of charge and there is no warranty for it, to the extent permitted by applicable law. * Except when otherwise stated in writing the copyright holders and/or other parties provide the program "as is" * without warranty of any kind, either expressed or implied, including, but not limited to, the implied warranties of * merchantability and fitness for a particular purpose. The entire risk as to the quality and performance of the program * is with you. Should the program prove defective, you assume the cost of all necessary servicing, repair or correction. * * The code is provided as an example and it's not covered by our support service. We won't be able to offer free support * in relation to it. Should you need a consultation, or assistance to customise this code, you can contact us to avail * of our paid consultation services: https://aelia.co/hire_us */ class Aelia_Live_Product_Sort { protected static $query_order = ''; protected static $query_limits = array(); /** * Fetches the prices of all the products in the catalogue. * * @param string currency * @return array */ protected static function fetch_product_prices(string $currency = null) { $transient_id = implode('_', array( 'wc_aelia_get_all_product_prices', $currency ?? get_woocommerce_currency(), WC_Cache_Helper::get_transient_version('product'), )); $product_prices = get_transient($transient_id); if($product_prices === false) { global $wpdb; $sql = " SELECT DISTINCT product_id FROM {$wpdb->wc_product_meta_lookup} "; $product_prices = array(); foreach(wp_list_pluck($wpdb->get_results($sql), 'product_id') as $product_id) { $product = wc_get_product($product_id); // Skip invalid products and variations if(!$product instanceof \WC_Product || $product instanceof \WC_Product_Variation) { continue; } $product_prices[$product_id] = array(); if($product instanceof \WC_Product_Variable) { $children_prices = array(); // Fetch the price of each variation foreach($product->get_visible_children() as $child_product_id) { $children_prices[] = apply_filters('wc_aelia_cs_get_product_base_price_in_currency', get_post_meta('_price', $child_product_id, true), $child_product_id); } $product_prices[$product_id]['min'] = !empty($children_prices) ? min($children_prices) : ''; $product_prices[$product_id]['max'] = !empty($children_prices) ? max($children_prices) : ''; } else { $product_prices[$product_id]['max'] = $product_prices[$product_id]['min'] = apply_filters('wc_aelia_cs_get_product_base_price_in_currency', get_post_meta('_price', $product_id, true), $product_id); } } if(is_array($product_prices)) { // Cache the products, for performance set_transient($transient_id, $product_prices, HOUR_IN_SECONDS); } } return $product_prices; } /** * Given a "LIMIT x, y" clause, returns an array with the start and length. * * @param string $limits * @return array */ protected static function extract_query_limits(string $limits): array { $limit_parts = explode(',', str_replace('LIMIT ', '', $limits)); $limit_parts = array_filter(array_map(function($value) { $value = trim($value); return is_numeric($value) ? (int)$value : ''; }, $limit_parts)); if(empty($limit_parts)) { return array( 'start' => 0, 'length' => null, ); } return array( 'start' => (count($limit_parts) > 1) ? array_shift($limit_parts) : 0, 'length' => array_shift($limit_parts), ); } /** * Overrides the limits set against a WP_Query, so that it always returns * all the products. * * @param string $limits * @param WP_Query $query * @return string */ public static function post_limits_request($limits, $query): string { self::$query_limits = self::extract_query_limits($limits); remove_filter('post_limits_request', array(__CLASS__, 'post_limits_request'), 10, 2); return ''; } /** * Filters the list of posts returned by a WP_Query, doing the following: * - Sorts them by the applicable price in the active currency. * - Returns only the posts that fall into the LIMITS set for the query. * * @param array $posts * @param WP_Query $wp_query * @return array */ public static function posts_results($posts, $wp_query) { remove_filter('posts_results', array(__CLASS__, 'posts_results'), 99, 2); $price_type = (self::$query_order === 'DESC') ? 'max' : 'min'; // Fetch all the product prices $product_prices = wp_list_pluck(self::fetch_product_prices(), $price_type); // Extract the numeric prices $numeric_prices = array_filter($product_prices, 'is_numeric'); // Sort the numeric prices in ascending or descending order if(self::$query_order === 'DESC') { arsort($numeric_prices, SORT_NUMERIC); } else { asort($numeric_prices, SORT_NUMERIC); } // Return a list of post, sorted as follows // - All the products with a numeric price, in ascending or descending order // - All the product with a non-numeric price, in any order $ordered_product_ids = array_flip(array_keys($numeric_prices + array_diff_key($product_prices, $numeric_prices))); uasort($posts, function($a, $b) use ($ordered_product_ids) { $a_pos = $ordered_product_ids[$a->ID] ?? -1; $b_pos = $ordered_product_ids[$b->ID] ?? -1; return $a_pos <=> $b_pos; }); return array_slice($posts, self::$query_limits['start'], self::$query_limits['length']); } /** * Intercepts the ordering arguments set against a WC_Query, to enable the sorting * by price in active currency. * * @param array $args * @param string $orderby * @param string $order * @return array */ public static function woocommerce_get_catalog_ordering_args($args, $orderby, $order) { if($orderby === 'price') { self::$query_order = $order; add_filter('post_limits_request', array(__CLASS__, 'post_limits_request'), 10, 2); add_filter('posts_results', array(__CLASS__, 'posts_results'), 99, 2); } return $args; } public static function init() { add_filter('woocommerce_get_catalog_ordering_args', array(__CLASS__, 'woocommerce_get_catalog_ordering_args'), 99, 3); } } Aelia_Live_Product_Sort::init();