Примеры отображения перечня разделов в PHP
Вывод списка разделов: основные подходы
Эффективное решение: построение иерархического списка с помощью PDO и рекурсии
Данный метод позволяет получить все строки из таблицы категорий одним запросом, затем средствами PHP организовать дерево и вывести его в виде вложенных списков. Это минимизирует количество запросов к базе и даёт полный контроль над структурой.
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$stmt = $pdo->query('SELECT id, title, parent_id FROM sections');
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Построение дерева
$tree = [];
foreach ($items as $item) {
$tree[$item['id']] = $item;
$tree[$item['id']]['children'] = [];
}
foreach ($items as $item) {
if ($item['parent_id'] && isset($tree[$item['parent_id']])) {
$tree[$item['parent_id']]['children'][] = &$tree[$item['id']];
}
}
// Рекурсивный вывод
function renderTree($items, $parentId = 0) {
echo '<ul>';
foreach ($items as $item) {
if ($item['parent_id'] == $parentId) {
echo '<li>' . htmlspecialchars($item['title']);
if (!empty($item['children'])) {
renderTree($item['children'], $item['id']);
}
echo '</li>';
}
}
echo '</ul>';
}
// Вызов для корневых элементов
renderTree($tree, 0);
?>
Пояснение шагов: Создаётся соединение PDO, выполняется запрос без параметров (выбираются все записи). Затем массив превращается в ассоциативный с ключами id, и для каждого элемента добавляется пустой массив children. Второй цикл заполняет children ссылками на дочерние элементы. Функция renderTree рекурсивно обходит дерево, начиная с parent_id = 0 (корневые категории).
Типичные проблемы: Отсутствие индекса parent_id снижает производительность при большом количестве записей. Глубокая рекурсия может превысить лимит вложенности функций (обычно 100-200 уровней). Рекомендуется использовать итеративный подход или увеличить лимит с помощью setXdebugMaxNestingLevel или переписать на стек. Также возможна путаница с кодировкой: данные должны поступать в UTF-8.
Как вывести плоский список разделов с отступами?
Если требуется просто показать все разделы с визуальным отступом, можно обойтись без рекурсии, используя сортировку по parent_id и хранение уровней.
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$stmt = $pdo->query('SELECT id, title, parent_id FROM sections ORDER BY parent_id, id');
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Массив для хранения уровня каждого id
$level = [];
$output = '<ul>';
foreach ($items as $item) {
$id = $item['id'];
$parent = $item['parent_id'];
$level[$id] = ($parent == 0) ? 0 : ($level[$parent] ?? 0) + 1;
$indent = str_repeat(' ', $level[$id]);
$output .= '<li>' . $indent . htmlspecialchars($item['title']) . '</li>';
}
$output .= '</ul>';
echo $output;
?>
Пояснение: Уровень вычисляется на основе родительского уровня, который уже был обработан (благодаря сортировке по parent_id). Отступы формируются повторяющимися символами неразрывного пробела.
Ошибки: Если родительский элемент ещё не обработан (при сортировке по id), уровень может быть неверным. Необходимо гарантировать, что родители идут раньше детей. Сортировка по parent_id, а затем по id обычно это обеспечивает.
Как решить задачу с помощью mysqli без PDO?
Для проектов, использующих устаревшее расширение mysqli, можно применить аналогичный подход с процедурным или объектным стилем.
<?php
$mysqli = new mysqli('localhost', 'user', 'pass', 'test');
$result = $mysqli->query('SELECT id, title, parent_id FROM sections');
$items = [];
while ($row = $result->fetch_assoc()) {
$items[] = $row;
}
// Далее построение дерева и вывод как в первом примере (аналогично)
?>
Пояснение: Разница только в способе получения данных: fetch_assoc() возвращает ассоциативный массив. Остальная логика построения дерева идентична.
Проблемы: При использовании mysqli необходимо вручную закрывать соединение и обрабатывать ошибки. Кроме того, сам запрос уязвим для SQL-инъекций, поэтому при наличии пользовательского ввода следует применять подготовленные запросы (mysqli_prepare).
Как получить список разделов из файловой системы (папки)?
Если разделы представлены каталогами на сервере, можно использовать функцию scandir или рекурсивный обход с DirectoryIterator.
<?php
$path = '/var/www/sections';
$dirs = array_filter(scandir($path), function($item) use ($path) {
return is_dir($path . DIRECTORY_SEPARATOR . $item) && $item !== '.' && $item !== '..';
});
echo '<ul>';
foreach ($dirs as $dir) {
echo '<li>' . htmlspecialchars($dir) . '</li>';
}
echo '</ul>';
?>
Пояснение: scandir возвращает все элементы директории, затем фильтром оставляются только папки. Для вложенности потребуется рекурсивная функция или RecursiveDirectoryIterator.
Ошибки: scandir не учитывает права доступа. Если папка недоступна, будет вызвано предупреждение. Для больших деревьев лучше использовать итераторы, которые не загружают всё в память.
Как применить рекурсивный CTE в MySQL для получения списка разделов?
Если используется MySQL 8.0 или новее, можно выполнить рекурсивный запрос, который возвращает все категории с уровнем вложенности.
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$sql = '
WITH RECURSIVE cte AS (
SELECT id, title, parent_id, 0 AS depth FROM sections WHERE parent_id = 0
UNION ALL
SELECT s.id, s.title, s.parent_id, cte.depth + 1 FROM sections s JOIN cte ON s.parent_id = cte.id
)
SELECT * FROM cte ORDER BY depth, parent_id, id
';
$stmt = $pdo->query($sql);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($items as $item) {
$indent = str_repeat(' ', $item['depth']);
echo '<li>' . $indent . htmlspecialchars($item['title']) . '</li>';
}
?>
Пояснение: CTE формирует иерархию прямо в базе данных, добавляя поле depth. PHP остаётся только вывести строки с отступами.
Проблемы: Не все версии MySQL поддерживают CTE. При большом количестве записей рекурсивный запрос может быть медленным без правильных индексов. Кроме того, глубина рекурсии в SQL может быть ограничена (по умолчанию 1000 уровней).
Расширенные примеры и нестандартные сценарии
<?php
// Пример 1: Вывод дерева с ссылками и подсветкой активного раздела
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$stmt = $pdo->query('SELECT id, title, parent_id, slug FROM sections');
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);
$tree = [];
foreach ($all as $item) {
$tree[$item['id']] = $item;
$tree[$item['id']]['children'] = [];
}
foreach ($all as $item) {
if ($item['parent_id'] && isset($tree[$item['parent_id']])) {
$tree[$item['parent_id']]['children'][] = &$tree[$item['id']];
}
}
function renderMenu($items, $parentId = 0, $currentId = 0) {
echo '<ul>';
foreach ($items as $item) {
if ($item['parent_id'] == $parentId) {
$active = ($item['id'] == $currentId) ? ' class="active"' : '';
echo '<li' . $active . '><a href="/' . htmlspecialchars($item['slug']) . '">' . htmlspecialchars($item['title']) . '</a>';
if (!empty($item['children'])) {
renderMenu($item['children'], $item['id'], $currentId);
}
echo '</li>';
}
}
echo '</ul>';
}
renderMenu($tree, 0, 3); // 3 - id активного раздела
?>
<pre class="ex_r">
Вывод: HTML-список с вложенными ul/li, активный пункт имеет класс active.
</pre>
// Пример 2: Плоский список с возможностью сворачивания (JavaScript не показан)
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$stmt = $pdo->query('SELECT id, title, parent_id FROM sections ORDER BY parent_id, id');
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
$level = [];
echo '<ul id="sortable">';
foreach ($items as $item) {
$level[$item['id']] = ($item['parent_id'] == 0) ? 0 : ($level[$item['parent_id']] ?? 0) + 1;
$dataLevel = $level[$item['id']];
echo '<li data-level="' . $dataLevel . '" data-id="' . $item['id'] . '">' . str_repeat(' - ', $dataLevel) . htmlspecialchars($item['title']) . '</li>';
}
echo '</ul>';
?>
<pre class="ex_r">
Результат: <ul><li data-level="0"...>Главная</li><li data-level="1"...> - Новости</li>...</ul>
</pre>
// Пример 3: Использование RecursiveDirectoryIterator для сканирования файловой системы с игнорированием скрытых папок
<?php
$path = '/var/www/sections';
$iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
$filter = new RecursiveCallbackFilterIterator($iterator, function($current) {
return $current->isDir() && substr($current->getFilename(), 0, 1) !== '.';
});
$recursive = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
$depth = 0;
echo '<ul>';
foreach ($recursive as $file) {
$d = $recursive->getDepth();
if ($d > $depth) {
echo '<ul>';
} elseif ($d < $depth) {
echo '</ul>';
}
echo '<li>' . htmlspecialchars($file->getFilename()) . '</li>';
$depth = $d;
}
echo str_repeat('</ul>', $depth + 1);
?>
<pre class="ex_r">
Вывод: вложенный список папок, исключая скрытые директории.
</pre>
// Пример 4: Генерация JSON для AJAX-подгрузки подкатегорий
<?php
header('Content-Type: application/json; charset=utf-8');
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'user', 'pass');
$parentId = (int) ($_GET['parent_id'] ?? 0);
$stmt = $pdo->prepare('SELECT id, title, (SELECT COUNT(*) FROM sections s2 WHERE s2.parent_id = s1.id) AS child_count FROM sections s1 WHERE parent_id = ?');
$stmt->execute([$parentId]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($items, JSON_UNESCAPED_UNICODE);
?>
<pre class="ex_r">
Возвращает JSON-массив объектов с полями id, title, child_count. Пример: [{"id":1,"title":"Электроника","child_count":5},...]
</pre>
// Пример 5: Кэширование дерева разделов с помощью файла
<?php
$cacheFile = __DIR__ . '/sections_cache.html';
$cacheTtl = 3600; // 1 час
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTtl) {
readfile($cacheFile);
exit;
}
ob_start();
// здесь код построения дерева как в первом примере
// ...
$html = ob_get_clean();
file_put_contents($cacheFile, $html);
echo $html;
?>
<pre class="ex_r">
При повторном обращении отдаётся сохранённый HTML, что снижает нагрузку на базу данных.
</pre>