Коротко: Решение задачи многоуровневой фильтрации для каталога медтехники с подкатегориями и вложенными подкатегориями. От идеи до кода — практика, трудности и рабочие решения.
Зачем нужна сложная фильтрация в каталоге медтехники?
Каталог медицинского оборудования — это сложная структура, где одни категории могут содержать подкатегории, а те, в свою очередь, — вложенные подкатегории. Например, категория Ларингоскопы включает подкатегорию Клинки для ларингоскопов, а внутри неё — вложенные подкатегории с конкретными типами игл или аксессуаров.
Как обеспечить удобный и интуитивный поиск среди сотен и тысяч товаров? Простая фильтрация по категориям не справится, когда нужна глубина и точность выбора. Пользователь должен видеть, выбирать и менять фильтры по всем уровням — и при этом фильтрация должна работать быстро и корректно, без сбоев и потери данных.
Основные технические вызовы
- Сохранение выбранных фильтров при переходе по страницам
- Передача фильтров AJAX-запросом без перезагрузки страницы
- Правильное отображение вложенных подкатегорий с чекбоксами, включая сохранение их состояния
- Оптимизация запросов к базе данных для работы с несколькими уровнями вложенности
Структура базы данных для медтехники с вложенными категориями
Для обеспечения гибкости и масштабируемости была разработана структура из трёх таблиц:
-- Категории (например, Ларингоскопы, Иглы, Биопсийные системы)
CREATE TABLE catalog_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
url VARCHAR(255) NOT NULL UNIQUE
);
-- Подкатегории с вложенностью (например, Клинки для ларингоскопов)
CREATE TABLE catalog_subcategories (
id INT AUTO_INCREMENT PRIMARY KEY,
category_id INT NOT NULL,
parent_id INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
url VARCHAR(255) NOT NULL UNIQUE,
FOREIGN KEY (category_id) REFERENCES catalog_categories(id),
FOREIGN KEY (parent_id) REFERENCES catalog_subcategories(id)
);
-- Вложенные подкатегории (например, Иглы для биопсии MAGNUM)
CREATE TABLE catalog_nested_subcategories (
id INT AUTO_INCREMENT PRIMARY KEY,
parent_subcategory_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
url VARCHAR(255) NOT NULL UNIQUE,
FOREIGN KEY (parent_subcategory_id) REFERENCES catalog_subcategories(id)
);
PHP: Формирование вложенной структуры категорий для отображения
Важной задачей было собрать все уровни вложенности из базы и передать в шаблон для вывода в виде многоуровневых чекбоксов. Вот пример функции, которая строит такую структуру:
function getCategoryGroupsWithSubcategories(): array {
global $link;
// Получаем категории
$catSql = "SELECT id, name FROM catalog_categories ORDER BY name";
$catRes = mysqli_query($link, $catSql);
$categories = [];
while ($c = mysqli_fetch_assoc($catRes)) {
$categories[$c['id']] = [
'id' => (int)$c['id'],
'name' => $c['name'],
'subcategories' => []
];
}
// Получаем подкатегории
$subSql = "SELECT id, name, category_id, parent_id FROM catalog_subcategories ORDER BY name";
$subRes = mysqli_query($link, $subSql);
$subIndex = [];
while ($s = mysqli_fetch_assoc($subRes)) {
$s['id'] = (int)$s['id'];
$s['category_id'] = (int)$s['category_id'];
$s['parent_id'] = $s['parent_id'] !== null ? (int)$s['parent_id'] : null;
$s['children'] = [];
$subIndex[$s['id']] = $s;
}
// Связываем вложенности подкатегорий
foreach ($subIndex as &$sub) {
if ($sub['parent_id'] !== null && isset($subIndex[$sub['parent_id']])) {
$subIndex[$sub['parent_id']]['children'][] = &$sub;
}
}
unset($sub);
// Вставляем только корневые подкатегории в категории
foreach ($subIndex as $subcat) {
if ($subcat['parent_id'] === null) {
$categories[$subcat['category_id']]['subcategories'][] = $subcat;
}
}
// Получаем вложенные подкатегории
$nestedSql = "SELECT id, parent_subcategory_id, name FROM catalog_nested_subcategories ORDER BY name";
$nestedRes = mysqli_query($link, $nestedSql);
$nestedIndex = [];
while ($n = mysqli_fetch_assoc($nestedRes)) {
$n['id'] = (int)$n['id'];
$n['parent_subcategory_id'] = (int)$n['parent_subcategory_id'];
$nestedIndex[$n['parent_subcategory_id']][] = $n;
}
// Привязываем вложенные подкатегории к подкатегориям
foreach ($categories as &$cat) {
foreach ($cat['subcategories'] as &$subcat) {
$subcat['nested_children'] = $nestedIndex[$subcat['id']] [];
}
}
unset($cat, $subcat);
return array_values($categories);
}
Как хранить выбранные фильтры и передавать между страницами
Ключевым моментом стала работа с сессиями и AJAX, чтобы пользователь мог выбрать фильтры, а результаты обновлялись без перезагрузки. В PHP сохраняем фильтры в сессии:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['catalog']['filter_categories'] = $_POST['categories'] [];
$_SESSION['catalog']['filter_nested_categories'] = $_POST['nested-subcategories'] [];
header('Location: /catalog/');
exit;
}
В JavaScript собираем выбранные чекбоксы по разным именам и отправляем на сервер через AJAX:
function findCategory() {
var selectedCategories = [];
$('input[name="categories[]"]:checked').each(function() {
selectedCategories.push($(this).val());
});
var selectedNested = [];
$('input[name="nested-subcategories[]"]:checked').each(function() {
selectedNested.push($(this).val());
});
$.ajax({
type: 'POST',
url: '/partner/setFilterCategories/',
data: {
categories: selectedCategories,
'nested-subcategories': selectedNested
},
dataType: 'json',
success: function(response) {
if (response.success) {
window.location.reload();
} else {
alert('Ошибка при применении фильтра');
}
},
error: function() {
alert('Ошибка AJAX запроса');
}
});
}
Фильтрация товаров по выбранным категориям и вложенным подкатегориям
Для получения товаров по выбранным фильтрам используется подготовленный SQL-запрос с динамическими условиями:
function getCatalogFiltered(array $categories, array $subcategories, array $nestedSubcategories, int $page, int $perPage): array {
global $link;
$offset = ($page - 1) * $perPage;
$sql = "
SELECT p.id, p.name, p.status,
c.name AS category_name,
sc.name AS subcategory_name,
nsc.name AS nested_subcategory_name
FROM catalog_products p
LEFT JOIN catalog_categories c ON p.category_id = c.id
LEFT JOIN catalog_subcategories sc ON p.subcategory_id = sc.id
LEFT JOIN catalog_nested_subcategories nsc ON p.nested_subcategory_id = nsc.id
WHERE p.status = 1
";
$params = [];
$types = '';
$conditions = [];
if (!empty($categories)) {
$placeholders = implode(',', array_fill(0, count($categories), '?'));
$conditions[] = "p.category_id IN ($placeholders)";
$params = array_merge($params, $categories);
$types .= str_repeat('i', count($categories));
}
if (!empty($subcategories)) {
$placeholders = implode(',', array_fill(0, count($subcategories), '?'));
$conditions[] = "p.subcategory_id IN ($placeholders)";
$params = array_merge($params, $subcategories);
$types .= str_repeat('i', count($subcategories));
}
if (!empty($nestedSubcategories)) {
$placeholders = implode(',', array_fill(0, count($nestedSubcategories), '?'));
$conditions[] = "p.nested_subcategory_id IN ($placeholders)";
$params = array_merge($params, $nestedSubcategories);
$types .= str_repeat('i', count($nestedSubcategories));
}
if (!empty($conditions)) {
$sql .= " AND (" . implode(' AND ', $conditions) . ")";
}
$sql .= " ORDER BY p.id DESC LIMIT ?, ?";
$params[] = $offset;
$params[] = $perPage;
$types .= 'ii';
$stmt = $link->prepare($sql);
if (!$stmt) {
die('Ошибка подготовки запроса: ' . $link->error);
}
$bindParams = [];
$bindParams[] = &$types;
foreach ($params as $key => &$value) {
$bindParams[] = &$value;
}
call_user_func_array([$stmt, 'bind_param'], $bindParams);
if (!$stmt->execute()) {
die('Ошибка выполнения запроса: ' . $stmt->error);
}
$result = $stmt->get_result();
return $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
}
Итоги и что дальше?
Сложность такой системы в том, чтобы удержать баланс между удобством пользователя и технической эффективностью. Многоуровневая вложенность позволяет делать поиск более точным, но требует внимательной реализации сохранения состояния фильтров и оптимизации запросов.
В этом проекте удалось решить основные задачи: фильтрация работает корректно на всех уровнях, состояние чекбоксов сохраняется, страницы пагинации считаются верно. Теперь пользователи легко находят нужное медицинское оборудование даже в глубокой структуре каталога.
Какие следующие шаги? Можно добавить фильтрацию по брендам, характеристикам, цене и остаткам на складах — всё это тоже требует продуманного подхода и опыта, который уже есть.
Почему этот опыт полезен вам?
Если вы работаете с большими каталогами, где нужна точная фильтрация по сложной иерархии, мой кейс поможет избежать типичных ошибок. Примените решения, которые проверены на реальном проекте с медицинским оборудованием — где ошибки в поиске критичны и недопустимы.
Готовы улучшить фильтрацию своего каталога?
Беру на себя технические задачи по фильтрации, сохранению состояния и оптимизации запросов. Помогу сделать так, чтобы ваш каталог работал как часы, а клиенты быстро находили нужное.
Свяжитесь для консультации и внедрения решений.