Вот и столкнулся я с тем, чтобы написать расширение функционала для 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');
}

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

  • Виктор

    Свойств товара много. Есть и числовые и строки. Думается, если числовой параметр, то выводить двухстороннюю линейку, а если строка, то просто чекбоксы. Вопрос, если автоматизировать , то как лучше, по вашему мнению, добавить колонку в 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 из первого пункта мы лишь подключили библиотеку, но функций там нет и в своем примере вы их не озвучиваете.

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

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

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

  • Алексей

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

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

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

  • Алексей

    Не хватает получения минимально и максимальной цены для слайдера

  • Влад

    Блин, так через жопу всё описано, читать тошно… Нихрена непонятно. Где чё выделено, если ничего не выделено… Один файл описывается, потом второй. Позже тот же… Какой тот же???

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

      Статья довольно старая, часть оформления потерялась при переезде на новый редактор блоков WordPress. Постараюсь поправить, как будет свободное время.

Добавить комментарий для Алексей Отменить ответ

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

*

*

*