/**
* 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();
Comments