Сайт вебмастера

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

05-04-2008Reading time ~ 5 min.PHP 19270

Получилось, что я немного меньше стал публиковать записей в блоге, но на это есть довольно веские причины. Помимо работы я постоянно занимаюсь 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 = "<ul>";
	$open  = "</li><li>";
	$close = "</li></ul>";
	$left  = "<ul><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;
}

Сорри, при переносе сайта удалились комменты, поэтому здесь, те, которые сохранились.


TedBeer :

6 апреля 2008 г. в 18:51 edit

Вот пример без 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("
", $out);
}

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

Galchenkov :

7 апреля 2008 г. в 11:19 edit

'Один', '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);

?>
Related Posts
Comments (2) RSS
1 Arreay 2009-07-07 21:26:56

Дааа, можно всё сделать на много проще :-) Над такой задачей я бился недели две тож пишу свою ЦМС


2 lorigin 2009-10-26 11:54:51

у меня была таблица [id name owner type]

owner - ID родителя [0 - корень]

type (0 если это каталог, 1 если конечный элемент), грубо говоря папка и файл

нужно было вывести дерево.

логика примерно такая:

- берем все где владелец КОРЕНЬ.

- запоминаем idшники этих записей, а саму выборку сохраняем в results[0] - тоесть хранит все элементы первого уровня

- теперь выбираем "второй уровень", тоесть у кого owner'ы - те что нашли раньше =)

- ну и т.д.

тоесть получаем массив results где каждый элемент массива содержит выборку по определенному уровню... количество запросов сколько уровней.. редко превышает 5

function WriteTree() {
    global $result; //делаем глобальной
    $in_string = '0'; // параметры запроса, тоесть сначала делаем выборку "WHERE owner in (0)"
    $result = array();
    $level = 0; // уровень дерева
    while ($in_string !='') {
        $query = "SELECT * FROM catalog WHERE owner in (".$in_string.") ORDER BY id ASC";
        $result[$level] = mysql_query($query) or die('ОШИБКА '.mysql_error());
        $in_string = '';
        $in_array = array();
        $num_rows = mysql_num_rows($result[$level]);
        if ($num_rows) { //если хотябы одна запись вернулась
            while ($catalog_row = mysql_fetch_array($result[$level])) {
                $in_array[] = $catalog_row['id'];  // выбираем ID текущего уровня, чтобы патом определить узлы следуюшей вложености
            }
            $in_string = implode(',',$in_array);
            $level++;
        }

    }
    unset ($result[$level]); // убиваем последнюю выборку, которая пустая
    //echo "уровней = ".$level;
    PrintBranch(0,0);
}

а тут разворачиваем дерево, заходим в каждую ветку и выводим элементы..

function PrintBranch($level,$id) {
    global $result;
    $ul_write = false;
    echo "\n";
    mysql_data_seek($result[$level], 0);
    while ($row = mysql_fetch_assoc($result[$level])) {
        if ($row['owner'] == $id) {
        //if (!$ul_write) echo "\n";
            echo "";
            echo "";
            echo "<b>".GetLangText($row['name'],'ru')."</b><b class=\"hide\">".GetLangText($row['name'],'en')."</b>";
            $new_lev = $level;
            if (isset($result[$level+1]) )
                PrintBranch(++$new_lev,$row['id']);
            echo "\n";
        }
    }
    echo "\n";

}