Кэширование мин. и макс. цены для всех категорий продуктов (Woocomerce)
Допустим у нас есть фильтр товаров для раздела (рубрики), в котором есть фильтрация по цене товара. Для такого фильтра удобно знать минимальную и максимальную цену товара в просматриваемой рубрике товаров...
Можно получать эту цену каждый раз при генерации страницы рубрики, но это тяжелый запрос и для производительности удобнее один раз получить цены для каждой рубрики и потом просто их использовать в фильтре.
Вариантов как закэшировать мин.макс. цену можно придумать много. В этой статье показан один из них. Так сделал я, когда столкнулся с такой задачей.
Принцип действия такой
Собираются все минимальные и максимальные цены товаров для каждой рубрики (для всех возможных уровней) и для таксономии в целом. Далее все эти данные сохраняются в WP опцию. Далее, при обновлении или добавлении товара все эти данные обновляются, но не сразу, а спустя 60 секунд после обновления записи (нужно это для того чтобы можно было массово обновлять записи и не проводить эту затратную операцию при каждом обновлении), время 60 сек. по идее можно увеличить до, например, 3600 (часа).
Другой подход: можно обновлять только данные отдельных рубрик в которых находится товар при обновлении/добавлении товара. И также при этом обновлять общие данные по всей таксе. Но в этом случае, нужно будет ставить костыль, на случай если товар был в одной рубрике, а при обновлении мы рубрику поменяли, нужно будет как-то вылавливать рубрику из которой был убран товар. Я выбрал вышеописанный подход - ибо в нем нет этих недостатков.
// кэширование мин. и макс. цены для рубрик продуктов // для тестирования ## получает и выводит данные на экран if( isset($_GET['get_minmax_prices_test']) ){ die( print_r( Minmax_Prices::get_data() ) ); } ## принудительно обновляет и выводит данные на экран if( isset($_GET['update_minmax_prices_test']) ){ Minmax_Prices::update_data(); die( print_r( Minmax_Prices::get_data() ) ); } ## Код кэширует минимальную и максимальную цену для ## каждой рубрики и по всем продуктам в целом. ## ver 1 // ставим обновление опции в очередь через минуту после обновления записи... add_action( 'save_post_product', ['Minmax_Prices','save_post_update'] ); add_action( 'deleted_post', ['Minmax_Prices','save_post_update'] ); // инициализация Minmax_Prices::check_for_update(); class Minmax_Prices { static $price_meta_key = 'price_reg'; // мета ключ в котором находится цена товара static $tax_name = 'product_cat'; // таксономия static $up_timeout = 60; // время в сек. после которого скрипт сработает при обновлении записи (продукта) static $minmax_option = 'product_cat_minmax_prices'; ## получает данные static function get_data(){ return get_option( self::$minmax_option, array() ); } static function check_for_update(){ $minmax_prices = self::get_data(); $uptime = & $minmax_prices['uptime']; if( empty($uptime) || time() > $uptime ) self::update_data(); } static function save_post_update(){ $minmax_prices = self::get_data(); $minmax_prices['uptime'] = time() + self::$up_timeout; update_option( self::$minmax_option, $minmax_prices ); } ## обновляет все данные minmax разом static function update_data(){ global $wpdb; // все рубрики со всеми включенными или нет записями в них $cat_data_sql = "SELECT term_id, object_id, parent FROM $wpdb->term_taxonomy tax LEFT JOIN $wpdb->term_relationships rel ON (rel.term_taxonomy_id = tax.term_taxonomy_id) WHERE taxonomy = '". esc_sql(self::$tax_name) ."'"; $cat_data = $wpdb->get_results( $cat_data_sql ); $origin_cat_data = $cat_data; // сохраним на всякий... // создадим новый массив, где ключом будет ID рубрики, а значение объект с данными parent // и всеми ID рубрик в массиве object_id (в рубрике записей может быть несколько...) $_cat_data = array(); foreach( $cat_data as $data ){ $_term = & $_cat_data[ $data->term_id ]; if( ! $_term ){ $_term = (object) array( 'parent' => $data->parent, 'object_id' => array(), ); } if( $data->object_id ) $_term->object_id[] = $data->object_id; } unset($_term); $cat_data = $_cat_data; // соберем дочерние рубрики в родительские в элемент 'child'. child будет PHP ссылкой на текущий элемент рубрики, чтобы добится // рекурсиии и многоуровневой вложенности. Так каждая рубрика будет содержать все данные о записях своей и всех уровней вложенных подрубрик... foreach( $cat_data as $term_id => $data ){ // есть родитель добавляем ссылку на этот элемент к родителю в элемент 'child' if( $data->parent ){ $_child = & $cat_data[ $data->parent ]->child; // для удобности... if( empty($_child) ) $_child = array(); $_child[] = & $cat_data[ $term_id ]; // ссылка } } unset( $_child ); // die( print_r( $cat_data ) ); // посмотреть что за монстр-массив у нас получился, без него код понять нереально :) // соберем все ID записей в один массив сэлементом вида: term_id => все ID записей из рубрики и всех уровней вложенных рубрик... $_cat_data = []; foreach( $cat_data as $term_id => $data ){ $prod_ids = array(); self::_recursion_collect_ids( $prod_ids, $data ); $_cat_data[ $term_id ] = array_unique( $prod_ids ); } $cat_data = $_cat_data; // ВСЕ! массив готов, обираем все MIN MAX данные $minmax_prices = []; $minmax_prices['uptime'] = time() + (DAY_IN_SECONDS / 2); // каждые пол дня // all - для всех товаров $mnimax_sql_base = "SELECT MIN( CAST(meta_value as UNSIGNED) ) as min, MAX(CAST(meta_value as UNSIGNED)) as max FROM $wpdb->postmeta WHERE meta_key = '". esc_sql(self::$price_meta_key) ."' AND meta_value > 0"; $minmax = $wpdb->get_row( $mnimax_sql_base, ARRAY_A ); $minmax_prices['all'] = implode( ',', $minmax ); // в разрезе рубрик foreach( $cat_data as $term_id => $prod_ids ){ if( empty($prod_ids) ) continue; $_IN_sql_list = implode(',', array_map('intval', $prod_ids) ); $mnimax_sql = "$mnimax_sql_base AND post_id IN( $_IN_sql_list )"; $minmax = $wpdb->get_row( $mnimax_sql, ARRAY_A ); // если есть хоть одно значение if( array_filter( $minmax ) ){ if( ! $minmax['min'] ) $minmax['min'] = $minmax['max']; // нулей быть не должно if( ! $minmax['max'] ) $minmax['max'] = $minmax['min']; // нулей быть не должно $minmax_prices[ $term_id ] = implode( ',', $minmax ); } } // обновляем update_option( self::$minmax_option, $minmax_prices ); } ## рекурсивно собарает object_id в указанный $collector static function _recursion_collect_ids( & $collector, $data ){ // добавим родные данные if( $data->object_id ){ if( is_array($data->object_id) ) $collector = array_merge( $collector, $data->object_id ); else $collector[] = $data->object_id; } // првоерим детей и там рекурсией... if( isset($data->child) ){ foreach( $data->child as $_data ){ self::_recursion_collect_ids( $collector, $_data ); // recursion //call_user_func_array( [__CLASS__, __METHOD__], [ $collector, $_data ] ); } } } }
Заметка: Код получился довольно сложный, но интересный с точки зрения программирования. Изначально я видел его проще, потому что не учел что нужно собирать ID записей по всем уровням вложенных подрубрик...
Код плохо подойдет для случаев, когда в магазине очень много товаров. Запрос собирает ID товаров из рубрики в IN() функцию MySQL, которая ограничена опцией max_allowed_packet, а также работает медленнее чем использование временной таблицы для этой цели (подробнее читайте здесь).
Я думаю для большинства магазинов такой код будет работать отлично!
Данные получаем так:
$minmax_data = Minmax_Prices::get_data(); /* Результат получим в таком виде, где ключ - это ID рубрики, а значение - это 'мин,макс' цена. 'all' содержит значение мин и макс цены всей таксы. Array( [uptime] => 1508719235 [all] => 80,68000 [1083] => 950,7300 [1084] => 1990,3970 [1085] => 200,3970 [1086] => 2000,3970 [1089] => 1990,1990 [1090] => 190,1990 [1091] => 1590,1990 ) */
Вместо заключения
Этот код подойдет не только для WooCommerce, но и для любого магазина на ВП. Суть: собрать все минимальные и максимальные цены из указанного метаполя для терминов всех уровней вложенности.