Мой сайт о WordPress и PHP С Днем победы!
5 апреля 2008

Как из дерева сделать ul-li структуру?

Читали 1364 раза
Рубрика: CMS
Навигация: Главная » CMS

Получилось, что я немного меньше стал публиковать записей в блоге, но на это есть довольно веские причины. Помимо работы я постоянно занимаюсь MaxSite CMS. Вот есть доказательства.

Столкнулся с одной проблемой, может кто поможет её решить. (Самому уже не хвататет уже ни терпения, ни ума).

Задача связана с выводом рубрик. Есть таблица рубрик, где есть поля:

  • category_id (номер рубрики)
  • category_id_parent (номер родителя)

Теоретически этого хватает, чтобы организовать древовидную структуру (массив).

Я её реализовал классическим способом: через рекурсию. То есть для поиска «детей» выполняется еще один SQL-запрос. Соответственно при большом количестве рубрик и её «ветвистости» получается довольно много SQL-запросов.

Частично эту проблему можно решить за счет кэширования: получается где-то раза в два меньше.

Погуглив я нашел еще один способ создания деревьев: в нем указывается отступ слева (level). Например так («Код 1»):

001			level = 0
	002		level = 1
	003		level = 1
		004	level = 2
	005		level = 1

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

 [1] => Array
        (
            [category_id] => 1
            [category_id_parent] => 0
            [category_name] => Новости
            [category_menu_order] => 4
            [parents] => 0
            [childs] =>
            [level] => 0
        )

    [3] => Array
        (
            [category_id] => 3
            [category_id_parent] => 0
            [category_name] => CodeIgniter
            [category_menu_order] => 1
            [parents] => 0
            [childs] => 6 5
            [level] => 0
        )

    [6] => Array
        (
            [category_id] => 6
            [category_id_parent] => 3
            [category_name] => Еще
            [category_menu_order] => 0
            [parents] => 3
            [childs] =>
            [level] => 1
        )
...

Это «одномерная» структура, где сразу указываются и level (отступ), и все «дети» (childs), и родитель (parents). Всей этой информации по идее должно хватить на то, чтобы выстраивать и получать любые рубрики.

Теперь, чтобы выстоить рубрики по уровню, вполне достаточно одного level. И в принципе сделать это с помощью str_pad - две строчки кода (результат будет выглядеть как «код 1»).

Но дальше - ступор... Потратил несколько дней, но так и не смог придумать, каким образом все это конвертировать в ul-li. Сложность в том, что простыми заменами кажется не обойтись из-за вложенности тэгов. Чтобы было совсем понятно, нужно конвертировать «Код 1» (или сам массив) вот в это:

  • 001
    • 002
    • 003
      • 004
    • 005

HTML-код:

<ul>
	<li>001
		<ul>
			<li>002</li>
			<li>003
				<ul>
					<li>004</li>
				</ul>
			</li>
			<li>005</li>
		</ul>
	</li>
</ul>

Кто-то знает как решить эту задачку?

[upd] Вот мой вариант решения, основанный только на level. Передполагается, что массив ($r) уже отсортирован в нужной последовательности.

function mso_tree_print($r)
{
	$out = "\n<ul>";
	$open  = "</li>\n<li>";
	$close = "</li>\n</ul>";
	$left  = "\n<ul>\n<li>";

	$old_level = 0;
	$open_li = 0;

	foreach ($r as $key=>$val)
	{
		$level = $val['level'];

		if ($level == $old_level)
		{
			$out .= $open . $val['category_name'];
		}
		elseif ($level > $old_level)
		{
			$open_li = $open_li + $level - $old_level;
			$out .= $left . $val['category_name'];

		}
		else // $level < $old_level
		{
			if (( $open_li) > 0)
				$out .= str_repeat($close, $open_li);

			$out .= $open . $val['category_name'];
			$open_li = 0;
		}
		$old_level = $level;
	}

	$out .= $close;
	$out = str_replace("<ul></li>", "<ul>", $out);
	return $out;
}
google.com bobrdobr.ru del.icio.us technorati.com linkstore.ru news2.ru rumarkz.ru memori.ru moemesto.ru

21 комментарий к “Как из дерева сделать ul-li структуру?”

  1. Алексей Саминский:

    Предположим, что массив упорядочен в порядке вывода.

    Вводим переменную current_level в начале он -1

    Запускаем цикл по массиву, при переходе к новой строке
    если:
    Level > current_level вставляем

    • и присваеваем current_level значение Level

      Level = current_level вставляем

    • и присваеваем current_level значение Level

      Level = current_level вставляем

    и присваеваем current_level значение Level

  2. Алексей Саминский:

    Извините, там редактор подгадил....

    Предположим, что массив упорядочен в порядке вывода.

    Вводим переменную current_level в начале он -1

    Запускаем цикл по массиву, при переходе к новой строке
    если:
    Level > current_level вставляем {ul}{li} и присваеваем current_level значение Level

    Level = current_level вставляем {/li}{li} и присваеваем current_level значение Level

    Level

  3. Максим:

    Удалил все бессмысленные комменты. Пока пытаюсь сделать, как Алексей предложил.

  4. TedBeer:

    Вот пример без level и без children(вычисляются по ходу дела) в базе. Порядок неважен, дети в массиве могут быть и до родителей, так что выборку из базы можно делать без сортировки.


    $info = Array(
    1 => Array
    (
    'category_id' => 1,
    'category_id_parent' => 0,
    'category_name' => 'Один'
    ),
    2 => Array
    (
    'category_id' => 2,
    'category_id_parent' => 1,
    'category_name' => 'Два'
    ),
    3 => Array
    (
    'category_id' => 3,
    'category_id_parent' => 1,
    'category_name' => 'Три'
    ),

    4 => Array
    (
    'category_id' => 4,
    'category_id_parent' => 3,
    'category_name' => 'Четыре'
    ),
    5 => Array
    (
    'category_id' => 5,
    'category_id_parent' => 0,
    'category_name' => 'Пять'
    ),
    6 => Array
    (
    'category_id' => 6,
    'category_id_parent' => 4,
    'category_name' => 'Шесть'
    )
    );

    $top = Array();

    foreach($info as &$item){
    $parentId = $item['category_id_parent'];
    $id = $item['category_id'];

    if( $parentId && isset($info[ $parentId])){
    if( !isset($info[ $parentId]['children']))
    $info[ $parentId]['children'] = Array($id);
    else
    $info[ $parentId]['children'][] = $id;
    } else {
    $top[] = $id;
    }
    }

    function glue( $in, &$info){

    $out = Array();
    foreach( $in as $id){
    $out[] = '<li>'. $info[$id]['category_name'];
    if( isset( $info[$id]['children'])){
    $out[] = '<ul>';
    $out[] = glue( $info[$id]['children'], $info);
    $out[] = '</ul>';
    }
    $out[] = '</li>';
    }
    return implode("\n", $out);
    }

    echo '<ul>' . glue( $top, $info) . '</ul>';

  5. Максим:

    Спасибо! :idea: То что нужно. Я правда сделал с помощью только level, тоже неплохо получается, правда массив должен сразу в нужном порядке идти. Так что новый вариант мне больше нравится. :cool:

  6. Galchenkov:

    Правильный вариант предложил Алексей Саминский. А какие ещё могут быть варианты? Все вычисления по возможности производить в базе. Конечно, такое хранение деревьев довольно дилетантское, но раз вы можете получить нужную структуру, то выводить надо в ul li ориентируясь на level.

    Чтобы не быть голословным, вот мой пример кода:

    'Один', 'level' => 0);
    $categories[] = Array('name' => 'Два', 'level' => 1);
    $categories[] = Array('name' => 'Три', 'level' => 1);
    $categories[] = Array('name' => 'Четыре', 'level' => 2);
    $categories[] = Array('name' => 'Пять', 'level' => 3);
    $categories[] = Array('name' => 'Шесть', 'level' => 2);
    $categories[] = Array('name' => 'Семь', 'level' => 0);

    // Инициализируем переменную
    $current_level = -1;

    foreach ($categories as $category)
    {
    // Уровень отображаемого категории (узла, если хотите)
    $category_level = $category['level'];

    if ($current_level <li>'.$category['name'];
    }

    if ($current_level &gt; $category_level)
    {
    // Тут важно закрыть все открытые ранее списки (ведь из третьего
    // уровня можно сразу попасть в первый)
    echo str_repeat('</li></ul>', $current_level - $category_level + 1);
    echo '<ul><li>'.$category['name'];
    }

    if ($current_level == $category_level)
    {
    echo '</li><li>' . $category['name'];
    }

    $current_level = $category_level;
    }

    // Тут тоже закрываем все открытые ранее списки
    echo str_repeat('</li></ul>', $current_level + 1);

    ?>

    П.С. Нормально хранить деревья можно (нужно) в nested sets. Но к вопросу отображения это ничего не даст. Просто на будущее...

  7. Максим:

    Ага, спасибо! У меня примерно так и получилось. Выложил в статью свой вариант.

  8. Galchenkov:

    Наши решения несколько разнятся. Как минимум, моё решение проще читается и не содержит ошибки. Так, например, для одного и того же кода, ваша функция возвращает отличное от моего отображение дерева. Хотя у меня отображается верно.

    Вот пример: http://programatica.ru/a.php

    Ещё, у вас довольно странные названия для функций. Функция с названием mso_tree_print НЕ ПЕЧАТАЕТ!?!? дерево. Её необходимо предварить echo или print (хотя название намекает на печать). Это очень некрасиво. Более того, входной параметр называется безлико $r, функция в названии имеет слово tree, а при формировании использует некий ключ category_name и просто level. Окрошка прям-таки. Совет (уж простите) - функцию переименовать, входному параметру дать осмысленное название, да и ключи (category_name, level) желательно брать из параметров (конечно, никто не запрещает их сделать по умолчанию таковыми).

    Ну и последнее =) Излишнее упоминать в ключах слово category. Да это и так понятно из контекста. Ну а коли употребляете, почему нет единообразия? Переименуйте level в category_level и т.п. Хотя я считаю что id, parent_id (а не id_parent), name и level предпочтительнее.

    Удачи!

  9. Максим:

    По именованиям - рабочий вариант. Как будет в итоге еще не решил. Ключи - это из БД, так что нет смысла из изменять.

    По вашей фунукции. Вы можете выложить её текст, а то WordPress какие-то куски съел?

  10. Galchenkov:

    Ключи - это из БД, так что нет смысла их изменять.

    =) Это говорит о том, что у вас и в БД бардак.

    Исходный код доступен всё по той же ссылке: http://programatica.ru/a.php

  11. Максим:

    Спасибо. Уже переделал по алгоритму TedBeer. Так универсальней и лучше получилось.

    Всем спасибо, вопрос закрыт.

    ps Можно также покритиковать и прочий бардак в моей CMS. Пишите на email, вышлю текущую версию.

  12. Galchenkov:

    Смысл критиковать, если для простого отображения древовидной структуры вы выбираете решение с вызовом рекурсивных функций и расчётом на стороне клиента (относительно СУБД, сервер это клиент) при этом утверждаете что лучше получилось.

    Выбранный вами метод будет работать быстро, за производительность я не робчу (хотя для кого-то это важно), но выглядит всё это очень страшно. По крайней мере для меня.

    П.С. Для ловли мух используйте мухобойку, а не пистолет или кувалду.

  13. Максим:

    Объясняю.

    У меня всего один запрос к БД. В результате я сразу получаю данные всех рубрик (из БД), а также рассчитываю три дополнительных поля
    - parents - номера всех родителей
    - childs - номера всех детей
    - level - уровень

    Таким образом, чтобы мне получить, скажем навигацию по рубрикам, вполне достаточно взять childs из этого массива. И не нужны никакие дополнительные действия. То же самое и parents. Как вы понимаете - это не что иное как навигация «хлебные крошки».

    В алгоритме с level есть один недостаток - нужно предварительно выполнять сортировку массива в правильном порядке. Рубрики же могут выводиться в разном: по номеру, названию, указанному menu_order и т.д. В случае с level придется искать еще и способы сортировки этого массива. В способе, который я выбрал, этого не требуется - sql-запрос сразу возвращает данные в нужном виде.

    Так что «страшно» и «кувалда» это скорее о level. :roll:

  14. Galchenkov:

    Для меня рубрики - это жёстко заданное пользователем дерево. Если вы его собираетесь сортировать, то возможно это не оптимальное решение.

    По поводу nested sets. Отличная вещь. Советую на досуге почитать. Всё дерево строится одним запросом. Легко получить ВСЕХ подчинённых у узла (подчинённых подчинённого и т.д.), ВСЮ ветвь от самого верхнего уровня где есть указанный узел. Легко получить всех родителей и т.п. Легко - значит одним запросом.

    Я часто разрабатываю интернет-магазины и хранение каталога в nested sets очень удобно.

  15. Максим:

    Ну может для интернет-магазина Nested Sets и подходит, но в моем случае это совершенно бесмыслено. После того, как получен массив всех рубрик, нет никакой необходимости выполнять дополнительные SQL-запросы. Так, что в моем случае их количество равно нулю. ;)

  16. Statist:

    Посмотри вот здеь http://www.internet-technologies.ru/articles/article_1281.html может чем-нибудь поможет

  17. Apple_Is_Apple:

    Извени за коммент не в тему.
    Я тут уже как 20 минут ищу на твоем сайте где можно скачать последнюю версию WP и как то не нахожу, блин.

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

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

    Либо я вообще не удачник, либо у тебя и правда нет такой юзабильной кнопки со своей сборкой.

    Так что, где доставать твои сборки.
    (Постоянный пользователь)

  18. Максим:

    У меня нет последней версии. ;) И не будет.

  19. Ranger:

    Ой эти споры кодеров :eek: смотреть страшно :smile:

  20. Настя:

    У мну получилось. Уря:)

  21. Web студия ILI.COM.UA:

    Преобразование sql выборки в древовидную структуру, печать древовидных структур....

    Сам я сталкивался с этой проблемой не один раз и каждый раз решал ее по-разному, с точки зрения высокого искусства, писанина мне моя абсол...


Оставьте комментарий! (Вы согласны с правилами)

 

:mrgreen: :neutral: :twisted: :arrow: :shock: :smile: :???: :cool: :evil: :grin: :idea: :oops: :razz: :roll: :wink: :cry: :eek: :lol: :mad: :sad: :!: :?:

При добавлении кода (html, php) заменяйте < на &lt; и > на &gt;.
Внимание: антиспам - зверь! Копируйте своё сообщение перед отправкой. На всякий случай.