Как реализовать рекурсивный вызов функции для обхода дерева рубрик
Что проверяют
Умеете ли вы превратить плоский список рубрик из базы в дерево и обойти его, не устроив сотню запросов. Следом часто спрашивают про глубину рекурсии, циклы в данных и альтернативы рекурсии.
Как отвечать
Начните с хранения. Обычно рубрики лежат в таблице со ссылкой на родителя (id, parent_id, name), у корней parent_id равен NULL. Дальше каркас:
- один запрос: все рубрики выбираются разом и группируются по родителю:
$byParent[$row['parent_id'] ?? 0][] = $row;; - рекурсивная функция:
buildTree(array $byParent, int $parentId = 0): arrayберёт детей узла и для каждого вызывает себя с егоid. Базовый случай: детей нет, возвращается пустой массив; - вывод: тот же обход рисует вложенные
<ul>, а с параметром$depthстроит плоский список с отступами для<select>; - типизация: вместо массивов можно вернуть объекты
readonly class Categoryс полемchildren, если проект на PHP 8.2 и новее.
Добавьте альтернативы. Итеративный обход через стек (SplStack или обычный массив) не упирается в глубину. Рекурсивный CTE (WITH RECURSIVE в MySQL 8 и PostgreSQL) достаёт поддерево одним запросом. Для частого чтения глубоких деревьев есть nested set и materialized path, в Laravel это пакет kalnoy/nestedset.
У PHP нет жёсткого лимита рекурсии, но с Xdebug срабатывает xdebug.max_nesting_level, а с PHP 8.3 переполнение стека даёт Error вместо падения процесса. Для рубрик в пять уровней это не актуально, но сказать стоит.
Где спотыкаются
Делают запрос к базе внутри рекурсии, по одному на узел. На каталоге из пятисот рубрик это пятьсот запросов ради одного меню.
Не защищаются от цикла в данных. Если рубрику по ошибке сделали потомком собственного потомка, рекурсия не остановится. Помогает множество посещённых id и проверка при сохранении.
Строят дерево ссылками через foreach ($items as &$item) и не делают unset($item) после цикла. Переменная остаётся ссылкой на последний элемент, и следующий цикл по тому же имени его перезаписывает.