Вот и столкнулся я с тем, чтобы написать расширение функционала для 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 не будет опубликован. Обязательные поля помечены *

*

*

*