Список секций PHP для контент-менеджмента
Эффективное получение списка разделов с использованием PDO
Для управления контентом часто требуется вывести перечень категорий или секций. Наиболее безопасный и производительный способ - применение расширения PDO с подготовленными запросами. Это исключает SQL-инъекции и упрощает работу с разными базами данных.
Как получить плоский список разделов из таблицы `sections`?
// Подключение к БД через PDO
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8';
$user = 'root';
$pass = '';
$opt = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
$pdo = new PDO($dsn, $user, $pass, $opt);
// Запрос на получение всех разделов
$stmt = $pdo->query('SELECT id, title, parent_id FROM sections ORDER BY parent_id, sort');
$sections = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Вывод
foreach ($sections as $section) {
echo $section['title'] . ' (id: ' . $section['id'] . ')';
}
Типичные ошибки:
- Использование устаревших функций mysql_*. Они удалены в PHP 7 и небезопасны.
- Отсутствие обработки исключений PDO - при ошибках соединения скрипт завершится без понятного сообщения.
- Неправильный порядок сортировки для вложенных разделов. Рекомендуется добавлять поле `sort` или использовать материализованные пути.
Решение:
Всегда включайте режим исключений (ERRMODE_EXCEPTION) и обрамляйте код в try-catch. Для вложенности используйте рекурсивные запросы или модифицированный обход дерева (Nested Sets).
Как использовать mysqli для листинга разделов?
Если проект уже работает с mysqli, можно обойтись без PDO. Главное - применять подготовленные запросы или хотя бы экранирование через real_escape_string.
$mysqli = new mysqli('localhost', 'user', 'pass', 'db');
$mysqli->set_charset('utf8');
$query = 'SELECT id, title FROM sections WHERE active = 1';
$result = $mysqli->query($query);
while ($row = $result->fetch_assoc()) {
echo $row['id'] . ' - ' . $row['title'] . "\n";
}
Проблемы:
- Забыли освободить результат ($result->free()) - лишнее потребление памяти.
- Использование query() без экранирования параметров открывает дверь для инъекций. Всегда применяйте подготовленные запросы, если есть внешние данные.
Как построить иерархический список разделов (дерево)?
Часто требуется показать вложенность категорий - например, родитель → подкатегория. Рекурсивный подход в PHP удобен, если число записей невелико.
function buildTree(array $elements, $parentId = 0) {
$branch = [];
foreach ($elements as $element) {
if ($element['parent_id'] == $parentId) {
$children = buildTree($elements, $element['id']);
if ($children) {
$element['children'] = $children;
}
$branch[] = $element;
}
}
return $branch;
}
// Получаем все разделы
$all = $pdo->query('SELECT id, title, parent_id FROM sections')->fetchAll(PDO::FETCH_ASSOC);
$tree = buildTree($all);
// Вывод в виде вложенного списка
function renderTree($tree, $level = 0) {
echo '';
foreach ($tree as $node) {
echo '- ' . str_repeat('--', $level) . $node['title'];
if (isset($node['children'])) {
renderTree($node['children'], $level + 1);
}
echo '
';
}
echo '
';
}
renderTree($tree);
Типичные ошибки:
- Рекурсия без ограничения глубины может привести к переполнению стека при большом количестве уровней.
- Отсутствие индексов на поле parent_id замедляет выполнение.
Рекомендации:
Для больших деревьев лучше использовать Nested Sets (модель вложенных множеств) или Materialized Path. Это позволяет получать поддерево одним запросом без рекурсии на PHP.
Как применить кэширование для ускорения вывода разделов?
Если список разделов редко меняется, выгодно кэшировать результат, чтобы не нагружать базу данных. Используем Memcached или Redis.
$memcached = new Memcached();
$memcached->addServer('localhost', 11211);
$key = 'sections_list';
$sections = $memcached->get($key);
if ($sections === false) {
// Данных в кэше нет - загружаем из БД
$stmt = $pdo->query('SELECT id, title, parent_id FROM sections');
$sections = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Сохраняем на 1 час
$memcached->set($key, $sections, 3600);
}
// Используем $sections
Критические моменты:
- После добавления или удаления раздела нужно сбрасывать кэш, иначе пользователи увидят устаревшие данные.
- При большом объёме данных сериализация/десериализация может быть затратной. Рассмотрите использование JSON-формата хранения.
Цель - снизить нагрузку на СУБД и ускорить ответ сервера. Особенно полезно на страницах с частым обращением, например, в меню сайта.
Как организовать постраничный вывод разделов?
При большом количестве записей целесообразно разбить список на страницы. Используем LIMIT и OFFSET, а также подсчёт общего числа строк.
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 20;
$offset = ($page - 1) * $perPage;
// Общее количество
$countStmt = $pdo->query('SELECT COUNT(*) FROM sections');
$total = $countStmt->fetchColumn();
// Текущая страница
$stmt = $pdo->prepare('SELECT id, title FROM sections ORDER BY id LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$sections = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Вывод разделов и пагинации
Распространённые ошибки:
- Использование LIMIT без сортировки приводит к неопределённому порядку записей на разных страницах.
- Пропуск OFFSET приводит к смещению данных: первая страница корректна, а вторая показывает те же записи.
- Незащищённая передача параметра page через GET может вызвать ошибку, если значение не является числом. Всегда проверяйте тип.
Пагинация удобна для административных панелей, где отображаются сотни разделов.
Как реализовать листинг с использованием ORM (например, Eloquent)?
В современных фреймворках обычно применяют ActiveRecord. Пример для Laravel:
use App\Models\Section;
// Все активные разделы
$sections = Section::where('active', 1)->orderBy('title')->get();
// Иерархический вывод через отношения
$sections = Section::with('children')->whereNull('parent_id')->get();
// Вывод в Blade
@foreach($sections as $section)
<li>{{ $section->title }}
@if($section->children->count())
<ul>
@include('partials.children', ['children' => $section->children])
</ul>
@endif
</li>
@endforeach
Проблемы при использовании ORM:
- Проблема N+1 - если не указать with('children'), каждый вызов отношения породит отдельный запрос.
- Избыточность - ORM может загружать все связанные модели, даже если нужны только названия.
Решение:
Всегда используйте жадную загрузку (with) для минимизации запросов. Для простых выборок применяйте select с конкретными полями.
Расширенные примеры кода для листинга разделов
Ниже приведены подробные сценарии, которые помогут разобраться в нюансах реализации.
Пример 1. Динамическое построение дерева с ограничением глубины
function buildTreeSafe($elements, $parentId = 0, $maxDepth = 10, $depth = 0) {
if ($depth > $maxDepth) {
return [];
}
$branch = [];
foreach ($elements as $element) {
if ($element['parent_id'] == $parentId) {
$children = buildTreeSafe($elements, $element['id'], $maxDepth, $depth + 1);
if ($children) {
$element['children'] = $children;
}
$branch[] = $element;
}
}
return $branch;
}
// Пример использования
$allSections = $pdo->query('SELECT id, title, parent_id FROM sections')->fetchAll(PDO::FETCH_ASSOC);
$tree = buildTreeSafe($allSections, 0, 5);
function renderTreeFlat($tree, $level = 0) {
foreach ($tree as $node) {
echo str_repeat(' ', $level * 4) . $node['title'] . PHP_EOL;
if (isset($node['children'])) {
renderTreeFlat($node['children'], $level + 1);
}
}
}
renderTreeFlat($tree);
Главная
Новости
Спорт
Политика
О сайте
Контакты
Результат показывает структуру с отступами. Функция buildTreeSafe предотвращает бесконечные циклы и переполнение стека при ошибочных данных.
Пример 2. Использование Nested Sets для плоского запроса дерева
Модель Nested Sets хранит левый и правый ключи для каждого узла. Тогда получить всё поддерево можно одним запросом без рекурсии.
// Предположим, таблица sections содержит поля lft и rgt
$parentId = 3; // ID корневого раздела
$stmt = $pdo->prepare('
SELECT child.id, child.title, child.lft, child.rgt
FROM sections AS parent
JOIN sections AS child ON child.lft BETWEEN parent.lft AND parent.rgt
WHERE parent.id = :id
ORDER BY child.lft
');
$stmt->execute([':id' => $parentId]);
$treeNodes = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Преобразуем в иерархию на PHP (используя стек)
function buildNestedSetTree($nodes) {
$stack = [];
$root = null;
foreach ($nodes as $node) {
$node['children'] = [];
$item = [$node];
$count = count($stack);
while ($count > 0 && $stack[$count - 1][0]['rgt'] < $node['lft']) {
array_pop($stack);
$count--;
}
if ($count == 0) {
$root = &$item;
} else {
$stack[$count - 1][0]['children'][] = &$item;
}
$stack[] = &$item;
}
return $root ? $root[0] : null;
}
$tree = buildNestedSetTree($treeNodes);
print_r($tree);
Array
(
[id] => 3
[title] => Категория А
[lft] => 2
[rgt] => 7
[children] => Array
(
[0] => Array
(
[id] => 4
[title] => Подкатегория А1
[lft] => 3
[rgt] => 4
[children] => Array()
)
...
)
)
Данный подход эффективен для частого чтения дерева, но вставка или удаление узлов требуют обновления множества записей.
Пример 3. Кэширование с динамическим сбросом через события
При использовании кэша Memcached важно инвалидировать данные после изменений. Реализуем триггер в виде метода модели.
class Section {
public static function getAllCached() {
$cache = new Memcached();
$cache->addServer('localhost', 11211);
$key = 'sections_all';
$data = $cache->get($key);
if (!$data) {
$data = self::fetchAll(); // запрос к БД
$cache->set($key, $data, 3600);
}
return $data;
}
public static function save($title, $parentId) {
// Вставка в БД
$pdo = getPDO();
$stmt = $pdo->prepare('INSERT INTO sections (title, parent_id) VALUES (?, ?)');
$stmt->execute([$title, $parentId]);
// Сброс кэша
$cache = new Memcached();
$cache->addServer('localhost', 11211);
$cache->delete('sections_all');
return $pdo->lastInsertId();
}
public static function delete($id) {
// Удаление из БД
$pdo = getPDO();
$stmt = $pdo->prepare('DELETE FROM sections WHERE id = ?');
$stmt->execute([$id]);
// Сброс кэша
$cache = new Memcached();
$cache->addServer('localhost', 11211);
$cache->delete('sections_all');
}
}
При каждом добавлении или удалении раздела кэш очищается, гарантируя актуальность данных.
Пример 4. Постраничный вывод с сохранением фильтра и сортировки
// Пример для административной панели
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, ['options' => ['default' => 1, 'min_range' => 1]]);
$perPage = 25;
$sortBy = $_GET['sort'] ?? 'title';
$allowedSorts = ['title', 'id', 'created_at'];
$sortBy = in_array($sortBy, $allowedSorts) ? $sortBy : 'title';
$order = strtoupper($_GET['order'] ?? 'ASC');
$order = in_array($order, ['ASC', 'DESC']) ? $order : 'ASC';
$offset = ($page - 1) * $perPage;
$countStmt = $pdo->query('SELECT COUNT(*) FROM sections WHERE active = 1');
$total = $countStmt->fetchColumn();
$stmt = $pdo->prepare("SELECT id, title, created_at FROM sections WHERE active = 1 ORDER BY $sortBy $order LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$sections = $stmt->fetchAll();
// Генерация ссылок пагинации
$totalPages = ceil($total / $perPage);
for ($i = 1; $i <= $totalPages; $i++) {
echo '' . $i . ' ';
}
Данный код учитывает безопасную вставку параметров сортировки через белый список и валидацию, исключая инъекции в ORDER BY.
Пример 5. Вывод разделов с помощью PDO и фильтром по статусу
$status = filter_input(INPUT_GET, 'status', FILTER_SANITIZE_STRING);
$allowedStatuses = ['active', 'inactive', 'draft'];
if (!in_array($status, $allowedStatuses)) {
$status = 'active';
}
$stmt = $pdo->prepare('SELECT id, title, status FROM sections WHERE status = :status ORDER BY id');
$stmt->execute([':status' => $status]);
$sections = $stmt->fetchAll(PDO::FETCH_OBJ);
foreach ($sections as $section) {
printf("%s (id=%d, status=%s)\n", $section->title, $section->id, $section->status);
}
Главная (id=1, status=active) Новости (id=2, status=active) Архив (id=3, status=inactive)
Использование FETCH_OBJ позволяет обращаться к полям как к свойствам объекта, что удобно в шаблонах.