Вот и столкнулся я с тем, чтобы написать расширение функционала для SimplaCMS. Какой же интернет-магазин не поддерживает сортировку и фильтрацию? Так вот почему то в одной из лучших систем управления отсутствует фильтр по цене! Есть все что угодно, кроме цены. Сегодня попробуем решить эту проблему, без покупки платных модулей.

Итак, поехали!

Фильтр будем делать красивый и удобный, для этого воспользуемся jquery-ui.

<!doctype html>
<html lang="en">
    <head>
	    <meta charset="utf-8">
	    <title>jQuery UI Slider - Range slider</title>
	    <link rel="stylesheet" href="//code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css">
	    <script src="//code.jquery.com/jquery-1.10.2.js"></script>
        <script src="//code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
	    <script>
	        $(function() {
	            $( "#slider-range" ).slider({
	                range: true,
	                min: 0,
	                max: 500,
	                values: [ 75, 300 ],
	                slide: function( event, ui ) {
	                	$("input[name=price_min]").val( ui.values[ 0 ] );
	                    $("input[name=price_max]").val( ui.values[ 1 ] );
	                }
	            });
	            $("input[name=price_min]").val( $( "#slider-range" ).slider( "values", 0 );
	            $("input[name=price_min]").val( $( "#slider-range" ).slider( "values", 1 );
	        });
	    </script>
    </head>
	<body>
		{*  Код вашего шаблона  *}
			{*  FORM фильтра  *}
				<p>
					<label>Фильтр по цене:</label>
					<div id="slider-range"></div>
				</p>
				<input type="text" name="price_min" value="" placeholder="От">
				<input type="text" name="price_max" value="" placeholder="До">
			
			{*  конец FORM фильтра  *}
		{*  Код вашего шаблона  *}
	</body>
</html>

Клиентскую часть можно править как угодно, у меня в дизайне ползунок, но можно и обойтись без javascript, просто сделать два поля с типом «text», и получать оттуда максимум и минимум цены, в рамках которых должен быть запрос.

Теперь собственно самое главное. Для начала надо разобраться как работает фильтр вообще. Как он ищет другие товары? За этим обратимся к той части системы управления, которая отвечает за отображение публичной части, а именно view/Products.php

// Товары
$products = array();
foreach($this->products->get_products($filter) as $p)
$products[$p->id] = $p;

Все 136 строк до этого формируется массив с данными фильтра. Какой товар, какой категории, сохранена ли сортировка в сессии, есть ли ключевое слово и т. п.

В выделенной 139 строке происходит обращение к классу из api/Products.php и его методу get_products, которому мы передаем все параметры фильтра. Там формируется запрос к базе данных и мы получаем именно тот список товаров, который нам нужен. Ни одного лишнего.

Собственно задача написания модуля свелась к тому, чтобы добавить в массив $filter данные с ценой, и заставить их влиять на запрос уже в API.

Все тот же файл. Выделенное — добавленное.

// GET-Параметры
$category_url = $this->request->get('category', 'string');
$brand_url = $this->request->get('brand', 'string');
$price_min = $this->request->get('price_min', 'string');
$price_max = $this->request->get('price_max', 'string');

Соответственно мы будем получать цену из GET параметров. В строке запроса получим что-то типа /goods?price_min=0&price_max=9000

<input type="text" name="price_min" value="">
<input type="text" name="price_max" value="">

Вот и поля, которые будут эту задачу выполнять в верстке сайта.

После проверки на пустоту $category_url и $brand_url вставляем код, который будет в фильтр добавлять значения минимальной и максимальной цены.

// Сортировка по цене
if (!empty($price_min))
$filter['price_min'] = $price_min;
if (!empty($price_max))
$filter['price_max'] = $price_max;

Далее работаем с API. Нужно:

  1. Добавить условие того, что если есть цена — добавляем к запросу условие, что цена в границах минимума и максимума;
  2. Собственно цена то как раз у нас в таблице с вариантами (__variants), а в запросе опрашиваются только __products и __brands. Надо добавить еще одну таблицу в запрос.

Файл api/products.php. Начнем со второго вопроса, а именно добавим в запрос еще одну таблицу

$query = "SELECT
p.id,
p.url,
p.brand_id,
p.name,
p.annotation,
p.body,
p.position,
p.created as created,
p.visible,
p.featured,
p.meta_title,
p.meta_keywords,
p.meta_description,
b.name as brand,
b.url as brand_url,
v.price as price
FROM __products p
$category_id_filter
LEFT JOIN __brands b ON p.brand_id = b.id
LEFT JOIN __variants v ON p.id = v.product_id
WHERE
1
$product_id_filter
$brand_id_filter
$features_filter
$keyword_filter
$is_featured_filter
$discounted_filter
$in_stock_filter
$visible_filter
$price_min_filter
$price_max_filter
GROUP BY p.id
ORDER BY $order
$sql_limit";

А теперь подробнее по выделенным строкам:

  • 124 — добавить строку, не забудьте добавить запятую в конце 123 строки
  • 128 — добавить строку;
  • 139-140 — переменные с условием;
  • 141 — заменить строку. Там должна быть переменная $group_by, так вот, она нам не понадобится

До этого в этом же методе можете удалить строку где есть

$group_by = "GROUP BY p.id";

А что в $price_min_filter и $price_max_filter?

А, ну да, чуть главное не забыл

До самого запроса необходимо вставить следующий код

// Фильтр по цене
if(!empty($filter['price_min']))
$price_min_filter = $this->db->placehold(' AND v.price > ?', intval($filter['price_min']));
if(!empty($filter['price_max']))
$price_max_filter = $this->db->placehold(' AND v.price < ?', intval($filter['price_max']) + 500 );

Да, последняя строчка ни что иное как маркетинговый ход. Согласитесь, что если человек ищет товар за 5 тысяч рублей, то может ему приглянется и за 5 500? Просто он об этом не знает.

Ну вот собственно и все.

Итого

Вид функции получения списка товаров с фильтром по цене:

public function get_products($filter = array())
	{		
		// По умолчанию
		$limit = 100;
		$page = 1;
		$category_id_filter = '';
		$brand_id_filter = '';
		$product_id_filter = '';
		$features_filter = '';
		$keyword_filter = '';
		$visible_filter = '';
		$visible_filter = '';
		$is_featured_filter = '';
		$discounted_filter = '';
		$in_stock_filter = '';
		$group_by = '';
		$order = 'p.position DESC';

		if(isset($filter['limit']))
			$limit = max(1, intval($filter['limit']));

		if(isset($filter['page']))
			$page = max(1, intval($filter['page']));

		$sql_limit = $this->db->placehold(' LIMIT ?, ? ', ($page-1)*$limit, $limit);

		if(!empty($filter['id']))
			$product_id_filter = $this->db->placehold('AND p.id in(?@)', (array)$filter['id']);

		if(!empty($filter['category_id']))
		{
			$category_id_filter = $this->db->placehold('INNER JOIN __products_categories pc ON pc.product_id = p.id AND pc.category_id in(?@)', (array)$filter['category_id']);
			$group_by = "GROUP BY p.id";
		}

		if(!empty($filter['brand_id']))
			$brand_id_filter = $this->db->placehold('AND p.brand_id in(?@)', (array)$filter['brand_id']);

		if(!empty($filter['featured']))
			$is_featured_filter = $this->db->placehold('AND p.featured=?', intval($filter['featured']));

		if(!empty($filter['discounted']))
			$discounted_filter = $this->db->placehold('AND (SELECT 1 FROM __variants pv WHERE pv.product_id=p.id AND pv.compare_price>0 LIMIT 1) = ?', intval($filter['discounted']));

		if(!empty($filter['in_stock']))
			$in_stock_filter = $this->db->placehold('AND (SELECT 1 FROM __variants pv WHERE pv.product_id=p.id AND pv.price>0 AND (pv.stock IS NULL OR pv.stock>0) LIMIT 1) = ?', intval($filter['in_stock']));

		if(!empty($filter['visible']))
			$visible_filter = $this->db->placehold('AND p.visible=?', intval($filter['visible']));
		// Фильтр по цене
		if(!empty($filter['price_min']))
			$price_min_filter = $this->db->placehold(' AND v.price > ?', intval($filter['price_min']));
		if(!empty($filter['price_max']))
			$price_max_filter = $this->db->placehold(' AND v.price < ?', intval($filter['price_max']) + 500 );

 		if(!empty($filter['sort']))
			switch ($filter['sort'])
			{
				case 'position':
				$order = 'p.position DESC';
				break;
				case 'name':
				$order = 'p.name';
				break;
				case 'created':
				$order = 'p.created DESC';
				break;
				case 'price':
				$order = 'pv.price IS NULL, pv.price=0, pv.price';
				$order = '(SELECT pv.price FROM __variants pv WHERE (pv.stock IS NULL OR pv.stock>0) AND p.id = pv.product_id AND pv.position=(SELECT MIN(position) FROM __variants WHERE (stock>0 OR stock IS NULL) AND product_id=p.id LIMIT 1) LIMIT 1)';
				break;
			}

		if(!empty($filter['keyword']))
		{
			$keywords = explode(' ', $filter['keyword']);
			foreach($keywords as $keyword)
				$keyword_filter .= $this->db->placehold('AND (p.name LIKE "%'.mysql_real_escape_string(trim($keyword)).'%" OR p.meta_keywords LIKE "%'.mysql_real_escape_string(trim($keyword)).'%") ');
		}

		if(!empty($filter['features']) && !empty($filter['features']))
			foreach($filter['features'] as $feature=>$value)
				$features_filter .= $this->db->placehold('AND p.id in (SELECT product_id FROM __options WHERE feature_id=? AND value=? ) ', $feature, $value);

		$query = "SELECT  
					p.id,
					p.url,
					p.brand_id,
					p.name,
					p.annotation,
					p.body,
					p.position,
					p.created as created,
					p.visible, 
					p.featured, 
					p.meta_title, 
					p.meta_keywords, 
					p.meta_description, 
					p.exportable, 
					b.name as brand,
					b.url as brand_url,
					v.price as price
				FROM __products p		
				$category_id_filter 
				LEFT JOIN __brands b ON p.brand_id = b.id
				LEFT JOIN __variants v ON p.id = v.product_id

				WHERE 
					1
					$product_id_filter
					$brand_id_filter
					$features_filter
					$keyword_filter
					$is_featured_filter
					$discounted_filter
					$in_stock_filter
					$visible_filter
					$price_min_filter
					$price_max_filter
				GROUP BY p.id
				ORDER BY $order
					$sql_limit";

		$query = $this->db->placehold($query);
		$this->db->query($query);

		return $this->db->results();
	}

Обновление 21 января 2016

Благодарим нашего читателя Романа за проявленную бдительность и внимательность.

Мы совсем упустили из виду постраничную навигацию. После внесенных изменений, количество страниц остается неизменным.

За постраничную навигацию отвечает функция count_products().

Вот как она должна выглядеть для работы с фильтром по цене:

public function count_products($filter = array()) {

		$category_id_filter = '';
		$brand_id_filter = '';
		$keyword_filter = '';
		$visible_filter = '';
		$price_min_filter = '';
		$price_max_filter = '';
		$is_featured_filter = '';
		$discounted_filter = '';
		$features_filter = '';
		
		if(!empty($filter['category_id']))
			$category_id_filter = $this->db->placehold('INNER JOIN __products_categories pc ON pc.product_id = p.id AND pc.category_id in(?@)', (array)$filter['category_id']);

		if(!empty($filter['brand_id']))
			$brand_id_filter = $this->db->placehold('AND p.brand_id in(?@)', (array)$filter['brand_id']);
		
		if(isset($filter['keyword']))
		{
			$keywords = explode(' ', $filter['keyword']);
			foreach($keywords as $keyword)
				$keyword_filter .= $this->db->placehold('AND (p.name LIKE "%'.mysql_real_escape_string(trim($keyword)).'%" OR p.meta_keywords LIKE "%'.mysql_real_escape_string(trim($keyword)).'%") ');
		}

		if(!empty($filter['featured']))
			$is_featured_filter = $this->db->placehold('AND p.featured=?', intval($filter['featured']));

		if(!empty($filter['discounted']))
			$discounted_filter = $this->db->placehold('AND (SELECT 1 FROM __variants pv WHERE pv.product_id=p.id AND pv.compare_price>0 LIMIT 1) = ?', intval($filter['discounted']));

		if(!empty($filter['visible']))
			$visible_filter = $this->db->placehold('AND p.visible=?', intval($filter['visible']));
		

		if(!empty($filter['price_min']))
			$price_min_filter = $this->db->placehold('AND v.price > ?', intval($filter['price_min']));
		if(!empty($filter['price_max']))
			$price_max_filter = $this->db->placehold('AND v.price < ?', intval($filter['price_max']));
		
		if(!empty($filter['features']) && !empty($filter['features']))
			foreach($filter['features'] as $feature=>$value)
				$features_filter .= $this->db->placehold('AND p.id in (SELECT product_id FROM __options WHERE feature_id=? AND value=? ) ', $feature, $value);
		
		$query = "SELECT count(distinct p.id) as count
				FROM __products AS p
				$category_id_filter
				LEFT JOIN __variants v ON p.id = v.product_id
				WHERE 1
					$brand_id_filter
					$keyword_filter
					$is_featured_filter
					$discounted_filter
					$visible_filter
					$price_min_filter
					$price_max_filter
					$features_filter ";

		$this->db->query($query);	
		return $this->db->result('count');
	}

 


10 комментариев

  • Свойств товара много. Есть и числовые и строки. Думается, если числовой параметр, то выводить двухстороннюю линейку, а если строка, то просто чекбоксы. Вопрос, если автоматизировать , то как лучше, по вашему мнению, добавить колонку в s_features, в котором отмечать как выводить или же делать это в другом месте, например, создать еще одну таблицу, вроде s_features_type?

    • Лучше брать все из одной таблицы. В противном случае придется дописывать код не только для вывода в каталоге и на карточке товара, но еще и в админке. Оно того не стоит. Вы сами потом запутаетесь.

      Дмитрий
  • Работает, но не правильно считает страницы.

    • Спасибо за Ваш комментарий! В самое ближайшее время, как только найду причину ошибки с постраничной навигацией, статья будет обновлена.

      Дмитрий
  • Нужно в count_products($filter = array())
    Добавить после visible
    // Фильтр по цене
    if(!empty($filter[‘price_min’]))
    $price_min_filter = $this->db->placehold(‘ AND v.price > ?’, intval($filter[‘price_min’]));
    if(!empty($filter[‘price_max’]))
    $price_max_filter = $this->db->placehold(‘ AND v.price < ?', intval($filter['price_max']));

    и после $query = "SELECT count(distinct p.id) as count
    FROM __products AS p
    $category_id_filter

    добавить
    LEFT JOIN __variants v ON p.id = v.product_id

    И еще после
    $visible_filter

    Добавить
    $price_min_filter
    $price_max_filter

    Тогда заработает как надо 🙂

    • Спасибо большое за сэкономленное время, коллега!

      Дмитрий
  • Здравствуйте,

    Скажите пожалуйста, а в сам шаблон как правильно разместить код и функции чтобы слайдер по цене все же работал как надо?

    Установив html из первого пункта мы лишь подключили библиотеку, но функций там нет и в своем примере вы их не озвучиваете.

    Можно ли дополнить статью полным кодом для шаблона.

    • Дополнил. Надеюсь все встало на свои места.

      Дмитрий Ильичев
  • Не работает слайдер вообще, он даже не отражается, просто голые инпуты, как будто бы стили и библиотеки не подгружены, хотя всё подгружено. Что с этим делать?

    Алексей
    • Искать ошибки и конфликты. Для начала, можно оставить без слайдера. Невозможно установить причину проблемы, не видя кода.

      Дмитрий Ильичев

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*

*

*