Категории товаров на PHP: выбор структуры данных для магазина

Раздел: Веб-разработка на PHP -> Категории товаров

Организация категорий товаров в PHP магазине

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

Как реализовать иерархические категории с помощью смежной модели (adjacency list)?

Самый распространённый способ – хранение ссылки на родительскую категорию в поле parent_id. Это простой и интуитивно понятный подход. Таблица categories может выглядеть так:

CREATE TABLE categories (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  parent_id INT DEFAULT NULL,
  FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE CASCADE
);

Shops category php (категории магазинов php)

Для построения дерева категорий обычно используется рекурсивная функция:

function getCategoriesTree($parentId = null) {
  $pdo = new PDO('mysql:host=localhost;dbname=shop', 'user', 'pass');
  $stmt = $pdo->prepare('SELECT * FROM categories WHERE parent_id ' . ($parentId === null ? 'IS NULL' : '= :parent_id'));
  if ($parentId !== null) $stmt->execute([':parent_id' => $parentId]);
  else $stmt->execute();
  $categories = $stmt->fetchAll();
  foreach ($categories as &$cat) {
    $cat['children'] = getCategoriesTree($cat['id']);
  }
  return $categories;
}

Category php shop (категория товара в магазине php)

Данный подход прост в реализации, но при большом количестве уровней может вызвать много запросов к базе данных. Проблему можно решить кешированием или построением дерева после одного запроса (например, с помощью сортировки по parent_id и обхода в памяти).
Цели: быстрая разработка, небольшой магазин с неглубокой иерархией.
Ошибки: забывают про индексы на parent_id, что замедляет запросы. Решение – добавить индекс: CREATE INDEX idx_parent ON categories(parent_id);

Типичная ошибка – рекурсия без ограничения глубины может вызвать бесконечный цикл при наличии циклической ссылки. Всегда стоит проверять parent_id на допустимость (например, при сохранении категории убедиться, что новый parent_id не является потомком самой категории).

Как быстро получать всех потомков и предков категории (модель nested set)?

Модель nested set (вложенные множества) добавляет поля lft (left) и rgt (right), которые определяют границы поддерева. Это позволяет одним запросом получить всех потомков категории.

CREATE TABLE categories_ns (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  lft INT NOT NULL,
  rgt INT NOT NULL
);
-- Пример вставки корневой категории
INSERT INTO categories_ns (name, lft, rgt) VALUES ('Корень', 1, 2);
-- Получить всех потомков категории с lft=2 и rgt=7
SELECT * FROM categories_ns WHERE lft > 2 AND rgt < 7 ORDER BY lft;

Недостатки модели – сложность вставки и удаления узлов: приходится пересчитывать lft/rgt у многих записей. Для магазинов, где категории меняются нечасто, это компромисс между производительностью чтения и сложностью записи.
Цели: частые запросы дерева, глубокие иерархии, редко изменяемые данные.
Проблемы: при большом количестве операций вставки/удаления могут возникать блокировки и снижение производительности. Решение – выполнять пересчёт в хранимой процедуре или отложенно.

Распространённая ошибка – неправильный пересчёт границ при добавлении нового узла. Например, вставка дочернего элемента должна обновить lft/rgt у всех родственных узлов, которые находятся правее. Легко допустить опечатку в SQL. Рекомендуется тестировать на небольшом наборе данных.

Как хранить путь категории в виде строки (materialized path)?

В модели materialized path для каждой категории запоминается полный путь от корня в виде строки, например, 1/4/7/. Поле path может быть VARCHAR с максимальной длиной, достаточной для глубины иерархии.

CREATE TABLE categories_mp (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  path VARCHAR(500) NOT NULL DEFAULT ''
);
-- Пример вставки
INSERT INTO categories_mp (name, path) VALUES ('Электроника', '1/');
INSERT INTO categories_mp (name, path) VALUES ('Телефоны', '1/2/');
INSERT INTO categories_mp (name, path) VALUES ('Смартфоны', '1/2/3/');
-- Получить всех потомков категории с path='1/2/'
SELECT * FROM categories_mp WHERE path LIKE '1/2/%' ORDER BY path;

Преимущество – простота запросов на чтение поддерева (like). Недостаток – изменение пути (например, перемещение категории) требует UPDATE всех потомков. Также сложнее получить непосредственных детей (нужно дополнительное условие на количество слешей).
Цели: простая реализация для неглубоких статичных иерархий, быстрый поиск по части пути.

Ошибка – использование LIKE '%...%' с ведущей звездой, что убивает индексы. Всегда нужно искать с фиксированного начала (LIKE '1/2/%'). Индекс на path не поможет, если шаблон начинается с %.

Дополнительные примеры и развёрнутые пояснения

1. Рекурсивная выборка с учётом активности товаров (adjacency list)

Пример
function getActiveCategoriesTree($parentId = null) {
  global $pdo;
  $sql = 'SELECT c.* FROM categories c
          JOIN products p ON c.id = p.category_id
          WHERE p.active = 1 AND c.parent_id ' . ($parentId === null ? 'IS NULL' : '= ?');
  $stmt = $pdo->prepare($sql);
  $params = ($parentId === null) ? [] : [$parentId];
  $stmt->execute($params);
  $result = [];
  foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
    $children = getActiveCategoriesTree($row['id']);
    if ($children || true) { // показываем категорию, если есть активные товары или потомки
      $row['children'] = $children;
      $result[] = $row;
    }
  }
  return $result;
}
Пример возвращаемого массива:
[
  [
    'id' => 1,
    'name' => 'Электроника',
    'parent_id' => null,
    'children' => [
      [
        'id' => 2,
        'name' => 'Телефоны',
        'parent_id' => 1,
        'children' => []
      ]
    ]
  ]
]

Пояснение: функция фильтрует категории, в которых есть активные товары. Если категория пуста, но имеет потомков с товарами, она всё равно показывается. Это позволяет избежать пустых разделов.

2. Построение выпадающего списка с вложенными уровнями (HTML select)

Пример
function buildOptions($categories, $depth = 0, $excludeId = null) {
  $html = '';
  foreach ($categories as $cat) {
    if ($excludeId && $cat['id'] == $excludeId) continue;
    $prefix = str_repeat('&nbsp;&nbsp;', $depth);
    if ($depth > 0) $prefix .= '- ';
    $html .= '<option value="' . $cat['id'] . '">' . $prefix . htmlspecialchars($cat['name']) . '</option>';
    if (!empty($cat['children'])) {
      $html .= buildOptions($cat['children'], $depth + 1, $excludeId);
    }
  }
  return $html;
}
Результат:
<option value="1">Электроника</option>
<option value="2">&nbsp;&nbsp;- Телефоны</option>
<option value="3">&nbsp;&nbsp;&nbsp;&nbsp;- - Смартфоны</option>

3. Хлебные крошки (breadcrumbs) с помощью materialized path

Пример
function getBreadcrumbs($path, $pdo) {
  $ids = explode('/', trim($path, '/'));
  $placeholders = implode(',', array_fill(0, count($ids), '?'));
  $stmt = $pdo->prepare("SELECT id, name FROM categories_mp WHERE id IN ($placeholders) ORDER BY path");
  $stmt->execute($ids);
  return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
Пример вызова для path='1/2/3/':
[
  ['id' => 1, 'name' => 'Электроника'],
  ['id' => 2, 'name' => 'Телефоны'],
  ['id' => 3, 'name' => 'Смартфоны']
]

4. Nested set: вставка нового листового узла

Пример
function insertNode($pdo, $parentId, $name) {
  // Получаем rgt родителя
  $stmt = $pdo->prepare('SELECT rgt FROM categories_ns WHERE id = ?');
  $stmt->execute([$parentId]);
  $parentRgt = $stmt->fetchColumn();
  // Сдвигаем все узлы, которые находятся правее места вставки
  $pdo->exec("UPDATE categories_ns SET rgt = rgt + 2 WHERE rgt >= $parentRgt");
  $pdo->exec("UPDATE categories_ns SET lft = lft + 2 WHERE lft > $parentRgt");
  // Вставляем новый узел
  $stmt = $pdo->prepare('INSERT INTO categories_ns (name, lft, rgt) VALUES (?, ?, ?)');
  $stmt->execute([$name, $parentRgt, $parentRgt + 1]);
}
После вставки: lft/rgt соседних узлов корректно увеличены.

Категория товара в магазине PHP - comments

En
Category php shop (php)