Вот и столкнулся я с тем, чтобы написать расширение функционала для 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. Нужно:
- Добавить условие того, что если есть цена — добавляем к запросу условие, что цена в границах минимума и максимума;
- Собственно цена то как раз у нас в таблице с вариантами
__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');
}