Построение дерева категорий на PHP. Рекурсия

Сегодня я расскажу, как на и MySQL создавать иерархическое дерево.

Такие деревья используются при построении категорий динамического сайта, например в интернет-магазине или при выводе комментариев к посту.

Вообще они строятся где только возможно. Главное правильно его построить и применить.

Самое главное, когда строишь иерархическое дерево — это правильная структура базы данных! Для примера рассмотрим структуру базы данных, где хранятся категории сайта. Для простого примера, таблица будет иметь 3 поля:

  • id — ключ категории
  • parent_id — id родительской категории
  • name – название раздела
  • Создадим таблицу, выполнив SQL-запрос в PHPMyAdmin:

    CREATE TABLE `categories` (`id` INT NOT NULL AUTO_INCREMENT , `parent_id` INT NOT NULL , `name` VARCHAR(50) NOT NULL , PRIMARY KEY (`id`));

    Теперь нужно заполнить нашу таблицу записями. В результате, должна получится примерно такая таблица:

    Можно заполнить тестовую таблицу запросом:

    INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (1, 0, "Раздел 1"), (2, 0, "Раздел 2"), (3, 0, "Раздел 3"), (4, 1, "Раздел 1.1"), (5, 1, "Раздел 1.2"), (6, 4, "Раздел 1.1.1"), (7, 2, "Раздел 2.1"), (8, 2, "Раздел 2.2"), (9, 3, "Раздел 3.1");

    И сейчас внимание! Дальше по логике нужно делать выборки из БД в цикле для выбора каждой категории и её подкатегории. НО! Ладно, если в БД несколько категорий, что тоже в принципе не правильно. А если сайт — интернет-магазин и у него сотня категорий и столько же подкатегорий? Тогда беда! Неведомое количество запросов к базе данных приведет к замедлению работы сайта или же к полному краху mysql-сервера.

    Можно используя только один запрос к БД выбрать все категории и ихние подкатегории.

    Сделаем запрос и сформируем удобный массив для дальнейшей работы.

    //Выбираем данные из БД $result=mysql_query("SELECT * FROM categories"); //Если в базе данных есть записи, формируем массив if (mysql_num_rows($result) > 0){ $cats = array(); //В цикле формируем массив разделов, ключом будет id родительской категории, а также массив разделов, ключом будет id категории while($cat = mysql_fetch_assoc($result)){ $cats_ID[$cat["id"]] = $cat; $cats[$cat["parent_id"]][$cat["id"]] = $cat; } }

    Выбираем все данные из таблицы categories и формируем ассоциативный массив $cats , ключем будет id родительской категорий.

    Сейчас будем строить дерево. Для построения будем использовать рекурсивную функцию .

    Иерархическое дерево будет иметь такую структуру:

    • Раздел 1
      • Раздел 1.1
        • Раздел 1.1.1
      • Раздел 1.2
    • Раздел 2
      • Раздел 1.1
      • Раздел 1.2
    • Раздел 3
      • Раздел 3.1

    Создадим рекурсивную функцию build_tree() . Она будет строить наше иерархическое дерево абсолютно любой вложенности.

    Function build_tree($cats,$parent_id,$only_parent = false){ if(is_array($cats) and isset($cats[$parent_id])){ $tree = "

      "; if($only_parent==false){ foreach($cats[$parent_id] as $cat){ $tree .= ""; } }elseif(is_numeric($only_parent)){ $cat = $cats[$parent_id][$only_parent]; $tree .= "
    • ".$cat["name"]." #".$cat["id"]; $tree .= build_tree($cats,$cat["id"]); $tree .= "
    • "; } $tree .= "
    "; } else return null; return $tree; }

    Функция принимает массив разделов и id раздела. В цикле перебираем подкатегории и если в них есть еще разделы, тогда функция запускается еще раз с новыми параметрами (новый массив разделов и id раздела, который нужно построить). Так формируется дерево любой вложенности!

    Для построения дерева, в коде прописываем:

    Echo build_tree($cats,0);

    Так вот в два шага мы создали иерархическое дерево разделов сайта и не важно сколько там разделов!

    UPD Если нужно дерево категорий в обратном порядке зная id категории, тогда нужно воспользоваться функцией:

    Function find_parent ($tmp, $cur_id){ if($tmp[$cur_id]["parent_id"]!=0){ return find_parent($tmp,$tmp[$cur_id]["parent_id"]); } return (int)$tmp[$cur_id]["id"]; }

    Данная функция принимает массив категорий, ключом которой есть id рубрики, и id категории от которой нужно идти вверх.

    Для построения такого дерева запускаем функцию build_tree c такими параметрами:

    Echo build_tree($cats,0,find_parent($cats_ID,ВАШ_ID_КАТЕГОРИИ));

    Есть вопросы? Задавайте в комментариях

    Сегодня наша цель, создать иерархическую структуру категорий. Нам важно чтобы было удобно хранить категории и чтобы было легко выводить их там где нам надо.

    Иногда простое кажется сложным, именно по этому выложу несколько фрагментов кода, которые я надеюсь вам пригодятся для реализации php категорий в виде дерева.

    Итак, структура должна состоять из id категории (id), из названия категории (name) и конечно id родительской категории (parent_id). В MySQL это выглядит так:

    CREATE TABLE IF NOT EXISTS `category` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `parent_id` int(11) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=0 ;

    Минимальная и понятная структура таблицы для хранения категорий.

    INSERT INTO `category` (`id`, `name`, `parent_id`) VALUES (1, "Телефоны и планшеты", "0"), (2, "Автомобили", "0"), (3, "Samsung", "1"), (4, "Apple", "1"), (5, "LG", "1"), (6, "Ford", "2"), (7, "Lexus", "2"), (8, "BMW", "2"), (9, "Galaxy Tab 4", "3"), (10, "Galaxy S6", "3");

    Там где значение parent_id=0, у данной категории нет родительской категории.

    Тут все понятно и просто. Теперь присутпим к выводу списка категорий. Но для правильного вывода списка, нам нужно сначала получить весь список php категорий, а уже потом с помощью рекурсии сформировать наше дерево. Следующая функция предназначена для получения этого списка:

    Function get_cat() { //запрос к базе данных $sql = "SELECT * FROM category"; $result = mysql_query($sql); if(!$result) { return NULL; } $arr_cat = array(); if(mysql_num_rows($result) != 0) { //В цикле формируем массив for($i = 0; $i < mysql_num_rows($result);$i++) { $row = mysql_fetch_array($result,MYSQL_ASSOC); //Формируем массив, где ключами являются адишники на родительские категории if(empty($arr_cat[$row["parent_id"]])) { $arr_cat[$row["parent_id"]] = array(); } $arr_cat[$row["parent_id"]] = $row; } //возвращаем массив return $arr_cat; } }

    //получаем массив каталога $result = get_cat();

    Теперь нужна функция с рекурсией

    Function view_cat($arr,$parent_id = 0) { //Условия выхода из рекурсии if(empty($arr[$parent_id])) { return; } echo "

      "; //перебираем в цикле массив и выводим на экран for($i = 0; $i < count($arr[$parent_id]);$i++) { echo "
    • " .$arr[$parent_id][$i]["name"].""; //рекурсия - проверяем нет ли дочерних категорий view_cat($arr,$arr[$parent_id][$i]["id"]); echo "
    • "; } echo "
    "; }

    Теперь осталось только вывести каталог на экран с помощью рекурсивной функции

    View_cat($result);

    И в общем то и всё. Таким образом мы можем получить полное дерево категорий с бесконечными подкатегориями.

    И так для начала опишу с чем мы будем работать и что нам понадобится.
    Система : PHP 5 и выше, mySQL 4 и выше
    Вспомогательные классы : dbsql.class.php (класс для работы с базой данных)
    Класс вложенных категорий : classTreeCategory.php (непосредственно основной класс, ниже приведен его листинг и пояснения.

    Создаем таблицу в БД, следующей структуры:

    Просмотр кода MYSQL

    В данной таблице присутствует поле ID — порядковый номер категории, podcat — имеет значение ноль у категорий первого порядка или ID родительской категории, name — название категории.

    Пример работы класса, вывод категорий списком с подкатегориями:

    Просмотр кода PHP

    include ("dbsql.class.php" ) ; include ("classTreeCategory.php" ) ; $DB = new DB_Engine("mysql" , $settings [ "dbHost" ] , $settings [ "dbUser" ] , $settings [ "dbPass" ] , $settings [ "dbName" ] ) ; // подключаемся к БД, с указанием данных доступа $category = new TreeCategory ($DB ) ; // передаем в класс категорий, объект работы с БД $category -> table = "category" ; // название таблицы в БД с категорийми $array = $category -> getCategory () ; // получаем все категории из БД в виде многоуровневого массива, отсортированные и вложенные уже в нужном нам порядке $category -> outCategory ($array , "option" ) ; // подготовка вывода категорий (формируем HTML), передаем массив с категориями echo $category -> html ; // вывод категорий в виде HTML name

    Как видно из примера выше, все предельно просто, создаем новый объект $category, устанавливаем с какой таблицей БД работаем: ‘category’, далее получаем из таблицы список всех категорий уже оформленный в виде массива и разложенных в иерархичном порядке, с учетом всех подкатегорий. затем передаем массив в метод outCategory() который формирует для нас готовый HTML код, который остается только вывести в браузер.

    Метод outCategory(), как мы видим принимает два параметра @array и @string в первом параметре массив со всеми категориями, а во втором строка содержащая значение option или table , это значени указывает какой тип HTML кода требуется сформировать.
    Значение option

    Просмотр кода HTML

    -категория1 --подкатегория 1 ---подподкатегория 1 -категория 2

    Для вставки данного HTML кода в поле select какой либо формы.

    Значение table — формирует следующий HTML код:

    Просмотр кода HTML

    Этот HTML код удобен для вставки в таблицу которая отображает все наши категории подкатегории.

    Класс имеет также следующие методы:
    deleteItem($id); — удаляет одну категорию, не смотря на вложенные
    delCategory($array, $id); — удаляет категорию со всеми вложенными подкатегориями, $array — массив со всеми категориями подготовленный методом $category->getCategory(), $id- номер удаляемой категории
    addItem(); — данный метод следует вызывать если вы хотите добавить категорию, при этом этот метод считывает значения из данных переданных методом POST, т.е. из массива $_POST.
    $name=$this->PHP_slashes(strip_tags($_POST[‘name’])); // имя категории
    $podcat=intval($_POST[‘podcat’]); // ID родительской категории, если указан 0 категория будет в корне.
    updateItem() ; — аналогично предыдущему методу, кроме того что данный метод обновляет категорию, её название и уровень вложенности.

    table="category"; // запрос на выборку списка категорий, название таблицы * $category->outCategory($category->getCategory()); // подготовка вывода категорий(запрос массива категорий) * echo $category->html; // вывод категорий в HTML name * */ /** * Дамп таблицы с которой ведется работа * * DROP TABLE IF EXISTS `category`; * CREATE TABLE `category` (* `id` int(11) NOT NULL auto_increment, * `podcat` int(11) NOT NULL, * `name` varchar(255) NOT NULL, * PRIMARY KEY (`id`), * KEY `id` (`id`) *) ENGINE=MyISAM DEFAULT CHARSET=utf8; * */ class TreeCategory { /** * Строка запроса в БД */ var $table; /** * Интерфейс работы с БД */ var $DB; /** * Массив категорий с вложенными подкатегориями */ var $arrayCat; /** * Авто-подстчет кол-ва прочерков перед названием категории при выводе */ var $countPodcat; /** * HTML код для вывода категорий с подкатегориями */ var $html; /** * Получаем интерфейс для работы с БД и кладем его в локальные переменную */ function __construct($DB) { $this->DB=$DB; $this->component=$_GET["component"]; } /** * Получает список категорий, сортирует и помещает в массив с вложенными массивами и т.д. * @return array category */ function getCategory () { $all = $this->DB->getAll("SELECT * FROM `{$this->table}` ORDER BY `id` ASC"); $path = array(); if(count($all)>0) { foreach($all as $item): if($item["podcat"]==0)$sort[$item["id"]]=$item; if($item["podcat"]>0) { if(isset($path[$item["podcat"]])) { $str="$sort"; foreach($path[$item["podcat"]] as $pitem): $rep=$item["podcat"]; $str.="[$pitem]"; endforeach; $str.="[{$item["podcat"]}]"; $str.="[{$item["id"]}]"; $str.="=$item;"; eval($str); foreach($path[$item["podcat"]] as $pitem): $path[$item["id"]]=$pitem; endforeach; $path[$item["id"]]=$item["podcat"]; } else { $sort[$item["podcat"]]["sub"][$item["id"]]=$item; $path[$item["id"]]=$item["podcat"]; } } endforeach; } $this->arrayCat=$sort; return $this->arrayCat; } /** * Печатает категории, помещает готовый HTML в $this->html * @param array Массив с категориями и вложенными подкатегориями * @param string Тип генерируемого HTML кода для вывода, option или table */ function outCategory(&$arrayCat, $type="option", $idSel=0) { foreach($arrayCat as $sub) { $this->countPodcat++; $this->outItem($sub, $type); if(!empty($sub["sub"]))$this->outCategory($sub["sub"], $type, $idSel); $this->countPodcat--; } } /** * Вспомогательный метод подготовки HTML кода * @param array Массив с категорией * @param string Тип генерируемого HTML кода для вывода, option или table */ function outItem($sub, $type="option", $idSel=0) { for($i=0;$icountPodcat;$i++) { $out.="-"; } if($idSel==$sub["id"])$se="selected"; else $se=""; if($type=="option")$this->html.=" {$out} {$sub["name"]} "; if($type=="table")$this->html.= {$out} {$sub["name"]} HTML; } function delCategory(&$a_tree,&$id=0) { foreach($a_tree as $sub) { if($sub["id"]$id and isset($sub["sub"]))$this->delCategory($sub["sub"],$id); if($sub["id"]==$id) { $sql="DELETE FROM {$this->table} WHERE id = "$id" LIMIT 1"; $this->DB->execute($sql); if (isset($sub["sub"])) $this->delCategory_process($sub["sub"]); } } } function delCategory_process(&$a_tree) { foreach($a_tree as $sub) { $sql="DELETE FROM {$this->table} WHERE id = "{$sub["id"]}" LIMIT 1"; $this->DB->execute($sql); if(isset($sub["sub"]))$this->delCategory_process($sub["sub"]); } } function updateItem() { $name=$this->PHP_slashes(strip_tags($_POST["name"])); $podcat=intval($_POST["podcat"]); $id=intval($_POST["id"]); $sql="UPDATE `{$this->table}` SET `name` = "{$name}",`podcat` = "{$podcat}" WHERE `id`="{$id}" LIMIT 1; "; $this->DB->execute($sql); } function addItem() { $name=$this->PHP_slashes(strip_tags($_POST["name"])); $podcat=intval($_POST["podcat"]); $id=intval($_POST["id"]); $sql="INSERT INTO `{$this->table}` (`id`,`podcat`,`name`) VALUES ("", "$podcat", "$name");"; $this->DB->execute($sql); } function deleteItem($id) { $id=intval($id); $sql="DELETE FROM `{$this->table}` WHERE `id` = "{$id}" LIMIT 1"; $DB->execute($sql); header("Location: ?component={$this->component}"); } function PHP_slashes($string,$type="add") { if ($type == "add") { if (get_magic_quotes_gpc()) { return $string; } else { if (function_exists("addslashes")) { return addslashes($string); } else { return mysql_real_escape_string($string); } } } else if ($type == "strip") { return stripslashes($string); } else { die("error in PHP_slashes (mixed,add | strip)"); } } }

    Весь класс писался в течении часа и разумеется имеет недочеты, но все этот поправимо. Его использование целесообразно в обучающих целях, хотя впрочем немного допилив его, вы сможете встроить его в любую систему и наслаждаться его работой)).

    Буду признателен если в комментариях вы предложите собственные варианты решения данной задачи — организации категорий бесконечного уровня вложенности.

    Ни один сайт не обходится без навигации или как еще называют "меню сайта". Так вот меню сайта бывает одноуровневым и многоуровневым в виде дерева. Если с одноуровневым меню особых сложностей в плане реализации не возникает, то при создании многоуровневого меню нужно хорошо подумать.

    Самое главное в этой задаче это спроектировать базу данных для нашего многоуровневого меню. Создадим таблицу Categories с тремя полями id , title , parent где:

    • ID - идентификатор
    • Title - Название меню
    • Parent - Родитель категории по умолчанию 0

    За ветвление меню отвечает поле Parent если Parent = 0 , то эта категория является родительской. Для того чтобы добавить потомков к родительской категории нужно в поле parent указать ID нужного родителя. Например:

    Таблицы с категориями

    Как видно из таблицы, у родительской категории Автомобили есть два потомка - это Мазда и Хонда связанных по полю Parent . А у категории Мотоциклы два потомка - это Кавасаки и Харлей . При этом у категории Лодки нет потомков. Надеюсь, что Вы поняли,как связать категории.

    Далее переходим от слов к практике. Создадим таблицу Categories. CREATE TABLE IF NOT EXISTS `categories` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `parent` int(10) unsigned NOT NULL, PRIMARY KEY (`id`)) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=20 ; -- -- Дамп данных таблицы `categories` -- INSERT INTO `categories` (`id`, `title`, `parent`) VALUES (1, "Автомобили", 0), (2, "Мотоциклы", 0), (3, "Мазда", 1), (4, "Хонда", 1), (5, "Кавасаки", 2), (6, "Харлей", 2), (7, "Мазда 3", 3), (8, "Мазда 6", 3), (9, "Седан", 7), (10, "Хечбэк", 7), (11, "Лодки", 0), (12, "Лифтбэк", 8), (13, "Кроссовер", 8), (14, "Белый", 13), (15, "Красный", 13), (16, "Черный", 13), (17, "Зеленый", 13), (18, "Мазда CX", 3), (19, "Мазда MX", 3); Алгоритм работы состоит из следующего: Создаем соединение с базой данных Результат работы Многоуровневое меню на PHP + MySQL для админки

    Если Вы хотите использовать данное меню в админке своего сайта, то нужно переписать пару функций tplMenu() , showCat() .

    Результат работы Выбери Автомобили → Мазда →→ Мазда 3 →→→ Седан →→→ Хечбэк →→ Мазда 6 →→→ Лифтбэк →→→ Кроссовер →→→→ Белый →→→→ Красный →→→→ Черный →→→→ Зеленый →→ Мазда CX →→ Мазда MX → Хонда Мотоциклы → Кавасаки → Харлей Лодки

    Очередная задача — сделать категории бесконечной вложенности средствами PHP. Облазив несколько сайтов я нашел много решений, но вот они не все доступны для моего понимания, т.к. уровень программирования там выше моего. Поэтому я нашел самое простое, возможно, не самое элегантное, но зато рабочее решение.

    Сначала давайте посмотрим, что у меня находится в БД

    • id — идентификатор нашей категории
    • category — название категории
    • category_id — идентификатор родительской категории. Можно было назвать parent_id, но так мне привычнее
    • lvl — уровень вложенности категории. Нужен для формирования ...
    • Все, что замазано — вам ни к чему, т.к. там информация нужная для меня лично, и чтобы вас не вводит в заблуждение — я затер эту ненужную инфу.
    Вывод категорий в

    Особо тут рассказывать нечего, если есть вопросы, то пишите в комментарии, а тем кому особо все равно, что здесь написано, предлагаю сразу посмотреть результат

    Если вы не поняли, то поле lvl в базе данных нам как раз нужно для того, чтобы знать, сколько "-" ставить перед названием категорий. Конечно, есть более изысканный метод без lvl, но на данный момент у меня только такое решение. Как что изменится — обновлю статью.

    Вывод категорий в
    • А вот вам другое решение, когда нужно вывести не в форму, а в список.

      Тут нам lvl уже не нужен, поэтому кода меньше. А ниже результат