Построение nested sets дерева состоящего из collapse\spoiler

122
26 октября 2019, 20:10

В БД имеется структура данных Nested sets и проект с использованием yii2/ Нужно отобразить эту структуру, в виде вложенных друг в друга Collapse.

Вот так выглядит структура отсортированная по RGT:

В итоге должно получится следующее:

-*spoiler*  
--*spoiler*  
--*spoiler*  
----*spoiler*  
-*spoiler*  
--*spoiler*   
----*spoiler*  
-*spoiler*   
-*spoiler*   
-*spoiler*   
-*spoiler*   
-*spoiler*   
-*spoiler*  
-*spoiler*  
--*spoiler*   
-*spoiler* 

Добавление спойлера происходит следующим образом. Это yii2 виджет, который возвращает верстку спойлеров, с уже наложенными стилями и проставленными id по которым будет осуществлятся js-сворачивание-разворачивание :

private static function createCollapse($label, $content)
{
    return Collapse::widget(
        [
            'items' => [
                [
                    //В таблице поле LABEL
                    'label' => $label,
                    //В качестве контента ID записи
                    'content' => $content
                ]
            ]
        ]
    );
}

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

<div class="panel panel-default">
    <div class="panel-heading">
        <h4 class="panel-title"><a class="collapse-toggle collapsed" href="#w7-collapse7" data-toggle="collapse" data-parent="#w7" aria-expanded="false">Main Spoiler</a></h4>
    </div>
    <div id="w7-collapse7" class="panel-collapse collapse" aria-expanded="false" style="height: 0px;">
        <div class="panel-body">
            <div id="w6" class="panel-group collapse in" aria-expanded="true" style="">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h4 class="panel-title"><a class="collapse-toggle collapsed" href="#w6-collapse1" data-toggle="collapse" data-parent="#w6" aria-expanded="false">Sub spoiler</a></h4>
                    </div>
                    <div id="w6-collapse1" class="panel-collapse collapse" aria-expanded="false" style="height: 0px;">
                        <div class="panel-body">
                            content
                        </div>
                    </div>
                </div>
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h4 class="panel-title"><a class="collapse-toggle collapsed" href="#w6-collapse2" data-toggle="collapse" data-parent="#w6" aria-expanded="false">Sub Spoiler</a></h4>
                    </div>
                    <div id="w6-collapse2" class="panel-collapse collapse" aria-expanded="false" style="height: 0px;">
                        <div class="panel-body">
                            content
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

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

Один из вариантов решения - одним запросом получить таблицу отсортированную по RGT, и проверять текущий LVL на 3 случая - больше\меньше\равно - пример реализации с <ul><li>. Но я никак не могу адаптировать этот пример под использование виджета :(

Второй вариант, как предложил @fedornabilkin, но nested sets не хранят в себе parent id, и опять же, возникает трудность с оборачиванием в Collapse::widget

Буду рад любой помощи!

Answer 1

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

$cats = [];
foreach($rows as $model){
    $cats[$model->parent][] = $model;
}

А затем этот массив скинуть в рекурсивный метод. Что-то типа такого для построения списков в виде дерева.

public static function createTree($cats, $parent)
{
    if(isset($cats[$parent]) && is_array($cats[$parent])) {
        $tree = '<ul>';
        foreach ($cats[$parent] as $model) {
            $tree .= '<li>' . $model->title;
            $tree .= self::createTree($cats, $model->id);
            $tree .= '</li>';
        }
        $tree .= '</ul>';
    }
    else{
        return null;
    }
    return $tree;
}

Во вьюшке вызываем метод echo className::createTree($cats, 1);

UPD
Для наглядности можно выполнить пример кода:

$rows = [];
$rows[] = ['id' => 1, 'title' => 'title 1', 'parent' => 0];
$rows[] = ['id' => 2, 'title' => 'title 2', 'parent' => 0];
$rows[] = ['id' => 3, 'title' => 'title 1 1', 'parent' => 1];
$rows[] = ['id' => 4, 'title' => 'title 1 2', 'parent' => 1];
$rows[] = ['id' => 5, 'title' => 'title 1 2 1', 'parent' => 4];
$rows[] = ['id' => 6, 'title' => 'title 1 2 2', 'parent' => 4];
$rows[] = ['id' => 7, 'title' => 'title 3', 'parent' => 0];
$rows[] = ['id' => 8, 'title' => 'title 3 1', 'parent' => 7];
$rows[] = ['id' => 9, 'title' => 'title 3 2', 'parent' => 7];
foreach($rows as $model){
    $cats[$model['parent']][] = $model;
}
function createTree($cats, $parent)
{
    if(isset($cats[$parent]) && is_array($cats[$parent])) {
        $tree = '<ul>';
        foreach ($cats[$parent] as $model) {
            $tree .= '<li>' . $model['title'];
            $tree .= createTree($cats, $model['id']);
            $tree .= '</li>';
        }
        $tree .= '</ul>';
    }
    else{
        return null;
    }
    return $tree;
}
echo createTree($cats, 0);

Answer 2

Итак, немного разобравшись с рекурсией и nested sets было написано вот такое решение:

  1. Получаем дерево, которое нужно отрисовать, одним запросом из бд:

    SELECT * FROM table WHERE ROOT = $root ORDER BY lft

  2. Далее, создадим вспомогательные функции для поиска потомков и фильтрации дерева по LVL:

    /**
     * Выделяет из дерева $tree потомков узла $node
     *
     * @param Tree[] $tree Массив узлов дерева для фильтрации
     * @param Tree $node Узел дерева потомки которого будут возвращены
     * @return Tree[]|[]
     */
    public static function filterChildren(array $tree, Tree $node)
    {
        return array_filter(
            $tree,
            function ($element) use ($node) {
                return $element->LFT > $node->LFT
                    && $element->RGT < $node->RGT
                    && $element->ROOT === $node->ROOT;
            }
        );
    }
    /**
    * Фильтрация дерева по параметру LVL;
    * Результирующий массив будет содержать только узлы с уровнем `$lvl`
    *
    * @param Tree[] $tree Массив узлов дерева для фильтрации
    * @param int $lvl Уровень по которому осущесвляется фильтрация
    * @return Tree[]
    */
    public static function filterByLvl(array $tree, $lvl)
    {
        return array_filter(
            $tree,
            function ($element) use ($lvl) {
                return $element->LVL === $lvl;
            }
        );
    }
  3. Метод для рендеринга коллапсов, по сути, для сокращения кода:

    private static function createCollapse(Tree $node, $content)
    {
        return Collapse::widget(
            [
                'items' => [
                    [
                        'label' => $node->NAME,
                        'content' => $content
                    ]
                ]
            ]
        );
    }
  4. Сама рекурсивная функция будет выглядеть вот так:

    /**
    * Рендеринг дерева в виде спойлеров.
    *
    * Метод возвращает HTML верстку дерева выполненную в виде вложенных друг в друга спойлеров.
    *
    * @param array $roots Массив узлов дерева для которых необходимо построить спойлеры
    * @param array $fullTree Полное дерево, включая детей и предков всех элементов
    *
    * @return string
    */
    public static function renderAsCollapses(array $roots, array $fullTree)
    {
        $collapses =  '';
        foreach ($roots as $key =>  $node) {
            /** @var Tree $node */
            //Проверка на наличие потомков
            if ($node->RGT - $node->LFT > 1) {
                $collapses .= self::createCollapse(
                    $node,
                    self::renderAsCollapses(
                        ClassifierTree::filterChildren($fullTree, $node),
                        $fullTree,
                    )
                );
            } else {
                $collapses .= self::createCollapse(
                    $node,
                    'content'
                );
            }
        }
        return $collapses;
    }

Использование:

    //$tree содержит массив элементов дерева, полученный из БД с помощью запроса
    //SELECT * FROM table WHERE ROOT = $root ORDER BY lft 
    $collapses = TreeViewHelper::renderAsCollapses(
        //Для корректной работы функции, первым аргументом необходимо передать
        //массив корневых элементов, для которого будет строится дерево.
        //Начинать построение можно от любого уровня вложенности.
        Tree::filterByLvl($tree, 1),
        $tree,
    );

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

Единственное, что меня смущает в этом решении, это вопрос о переполнении стэка. На сколько большой должна быть вложенность, что бы переполнился стэк?

READ ALSO
Как вставить html код fpdf

Как вставить html код fpdf

Всем привет, подскажите пожалуйста хочу создать pdf файл, есть html таблица со стилямиКак мне вставить мой html код?

132
Удалить все символы кроме букв и цифр [дубликат]

Удалить все символы кроме букв и цифр [дубликат]

На данный вопрос уже ответили:

159
Куда подключить данные In auth manager. Yii2

Куда подключить данные In auth manager. Yii2

Работаю с фреймворком Yii2Установил widget, https://github

129
Пустые переменные в общей строке PHP

Пустые переменные в общей строке PHP

Получаю переменные из формы(Они 100% поступают, проверял)

152