WordPress как на ладони
WordCamp Saint Petersburg 2018 wordpress jino

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

Кэширование мин. и макс. цены для всех категорий продуктов (Woocomerce) 2 коммента
  • Тимур и Дмитрий отличная статья good
    Когда планируете запустить тематическое видео(WP) на канале?

    P.s.https://www.youtube.com/channel/UCDSCz9freb5xCwobaumlLkA/featured

    Ответить2 месяца назад #

Здравствуйте, !

Ваш комментарий
Предпросмотр