Как из дерева сделать ul-li структуру?
05-04-2008Время чтения ~ 5 мин.PHP 20034
Получилось, что я немного меньше стал публиковать записей в блоге, но на это есть довольно веские причины. Помимо работы я постоянно занимаюсь 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 > $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);
?>
Дааа, можно всё сделать на много проще :-) Над такой задачей я бился недели две тож пишу свою ЦМС
у меня была таблица [id name owner type]
owner - ID родителя [0 - корень]
type (0 если это каталог, 1 если конечный элемент), грубо говоря папка и файл
нужно было вывести дерево.
логика примерно такая:
тоесть получаем массив 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"; }