Кешування мін. та макс. ціни для всіх категорій продуктів (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, а й для будь-якого магазину на ВП. Суть: зібрати всі мінімальні та максимальні ціни із зазначеного метаполя для термінів усіх рівнів вкладеності.