Vkontakte
+7 (499) 705-13-20
skype: kuzma.feskov

Библиотека для работы с деревьями Nested Sets

Автор: Кузьма Феськов, 21 июля 2015

Класс базируется на библиотеке Максима Полторака phpDBTree 1.4

Текущая версия 4.4 (от 21 июля 2015)

GitHubhttps://github.com/kvf77/DbTree

загрузить

Список изменений:

v4.4 - Метод MakeUlList изменен - расширена функциональность

v4.3 - добавлен новый метод MakeUlList

v4.2 - добавлены примеры кода, демонстрирующие все возможности библиотеки

v4.1 - добавлен новый метод SortChildren, правки документации

Введение

Основной особенностью библиотеки является, то, что все запросы в методах переписаны согласно стандартам ANSI и работают без изменений на подавляющем большинстве баз данных.

При работе с базой данных библиотека использует класс safemysql.class.php Романа Шевченко.

Документация

Инициализация класса

<?php
    $tree_params = array(
        'table' => 'sections',
        'id' => 'sections_id',
        'left' => 'sections_left',
        'right' => 'sections_right',
        'level' => 'sections_level'
    );

    $db = new SafeMySQL($dsn);
    $tree = new DbTreeExt($tree_params, $db);
?>

Где $db - это объект класса для работы с базой данных, в нашем случае safemysql.class.php. А $tree_params - это перечень основных служебных полей таблицы, в которой мы будем хранить наше дерево.

Все, теперь у нас есть полностью настроенное дерево, с которым мы можем работать.

Конкретные примеры использования

В качестве практического примера я рассмотрю возможность хранения в рамках одной таблицы нескольких деревьев. Также мы предположим, что структура нашего сайта хранится в виде дерева Nested Sets. Мы посмотрим, какие возможности предоставляет нам библиотека для работы с сайтом и для его обслуживания.

И так, приступим.

Исходные данные: мы будем использовать дерево для хранения структуры сайта (то есть перечень его разделов). Чтобы усложнить задачу, предположим, что сайт у нас поддерживает 2 языка. Это значит, что структур сайта у нас будет 2. Чтобы не множить таблицы, мы оба дерева (русское и английское) будем хранить в одной таблице.

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

Создаем таблицу

CREATE TABLE `sections` (
  `section_id` int AUTO_INCREMENT NOT NULL,
  `section_left` int NOT NULL default '0',
  `section_right` int NOT NULL default '0',
  `section_level` int default NULL,
  `section_lang` varchar(2) NOT NULL default '',
  `section_name` varchar(255) NOT NULL default '',
  `section_path` varchar(255) NOT NULL default '',
  `section_full_path` varchar(255) default NULL,
  PRIMARY KEY  (`section_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8

Для начала, создадим таблицу, для хранения деревье в базе данных.

Первые 4 поля имеют отношения непосредственно к структуре дерева и напрямую в них вмешиваться не следует.

  • section_lang служит семафором, указывая на принадлежность элемента к определенному дереву.
  • section_name – имя раздела для вывода его пользователю.
  • section_path – минимальный элемент пути к разделу, например, sample.
  • section_full_path будет содержать полный путь по сайту до указанного элемента, например, my_site/sample/.

Инициализируем наши деревья

<?php
    $tree_params = array(
        'table' => 'sections',
        'id' => 'sections_id',
        'left' => 'sections_left',
        'right' => 'sections_right',
        'level' => 'sections_level'
    );
    
    $db = new SafeMySQL($dsn);
    $dbtree = new DbTreeExt($tree_params, $db);

    // Русское дерево
    $data = array(
        'section_left' => 1,
        'section_right' => 2,
        'section_level' => 0,
        'section_lang' => 'ru',
        'section_name' => 'Root',
        'section_path' => '',
        'section_full_path' => ''
    );

    $sql = 'INSERT INTO sections SET ?u';
    $db->query($sql, $data);

    // Английское дерево
    $data = array(
        'section_left' => 1,
        'section_right' => 2,
        'section_level' => 0,
        'section_lang' => 'en',
        'section_name' => 'Root',
        'section_path' => '',
        'section_full_path' => ''
    );

    $sql = 'INSERT INTO sections SET ?u';
    $db->query($sql, $data);
?> 

Теперь наша таблица содержит все первоначальные данные и мы можем работать.

Добавление элементов в структуру сайта

Какие этапы нам нужно пройти?

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

Разумеется, есть еще подэтапы проверки введенных пользователем данных, но мы их опустим, как опустим 3 этап, потому что он не имеет отношения к решаемой проблеме. Мы могли бы опустить и 2 этап, но так как нам он часто будет нужен, я приведу его 1 раз и далее буду ссылаться на этот пример.

<?php
    // Первый и второй этапы
    // Инициализируем класс
    $tree_params = array(
        'table' => 'sections',
        'id' => 'sections_id',
        'left' => 'sections_left',
        'right' => 'sections_right',
        'level' => 'sections_level'
    );
    
    $db = new SafeMySQL($dsn);
    $dbtree = new DbTreeExt($tree_params, $db);
    
    // Получаем все дерево для русской версии сайта
    $sections = $dbtree->Full(
        array(
            'section_id',
            'section_level',
            'section_name',
            'section_lang'
        ),
        array(
            'and' => array(
                'section_lang = "ru"'
            )
        )
    );
    
    // Обрабатываем полученное дерево
    foreach ($sections as $item) {
        // Делаем отступы (лесенка) согласно уровню вложенности
        $item['spacer'] = str_repeat(' ', 6 * $item['section_level']);
    
        // Печатаем на экран дерево с правильными отступами
        echo $item['spacer'] . $item['section_name'] . '<br />';
    }
    
    // Выводим массив $sections на экран,
    // чтобы дать возможность пользователю выбрать,
    // у какого раздела создать подраздел.
?>

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

А мы переходим к 4 этапу. Данные получены, проверены и нам надо на их основе добавить новый раздел. Давайте перечислим, какие данные нам нужны, чтобы добавить новый раздел:

  • section_id – номер раздела К КОТОРОМУ добавляем новый подраздел,
  • section_name – имя раздела,
  • section_lang – идентификатор языковой версии дерева,
  • section_path – отрезок пути для данного конкретного раздела.

При добавлении нового раздела нам необходимо рассчитать section_full_path. Для этого нам необходимо получить всех родителей добавляемого раздела и сложить их section_path в единый путь.

Разместим все эти данные для удобства в массиве $section: $section['section_id'], и так далее.

<?php
    $tree_params = array(
        'table' => 'sections',
        'id' => 'sections_id',
        'left' => 'sections_left',
        'right' => 'sections_right',
        'level' => 'sections_level'
    );
    
    $db = new SafeMySQL($dsn);
    $dbtree = new DbTreeExt($tree_params, $db);
    
    // На входе данные для создания новой ветки:
    $section = array(
        'secton_id' => xxx, // ID РОДИТЕЛЯ, к которому добавляем ветку
        'section_lang' => 'ru', // языковая версия дерева
        'section_name' => 'new child', // Название нового раздела
        'section_path' => 'newchild', // Кусок пути для этого раздела
        'section_full_path' => '' // Здесь пока пусто - нужно рассчитать
    );
    
    // $section_id - id родителя, к которому добавляем новый раздел
    // $section_lang - языковая версия дерева
    // $section
    
    // Получаем всех родителей раздела для построения полного пути
    $data = $dbtree->Parents($section_id, array(
            'section_level',
            'section_path'
        ), array(
            'and' => array(
                'section_lang = "ru"'
            )
        )
    );
    
    // Используя полный список родителей -
    // строим полный путь до нового раздела
    foreach ($data as $item) {
        if (0 <> $item['section_level']) {
            $section['section_full_path'] .= $item['section_path'] . '/';
        }
    }
    
    $section['section_full_path'] .= $section['section_path'] . '/';
    
    // Добавляем новый раздел
    $id = $section['section_id'];
    unset($section['section_id']);
    
    // Вставляем новую ветку в дерево
    $id = $dbtree->Insert($id, array(
            'and' => array(
                'section_lang = "' . $section['section_lang'] . '"'
            )
        ), $section
    );
    
?>

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

Удаление раздела

<?php
    $tree_params = array(
        'table' => 'sections',
        'id' => 'sections_id',
        'left' => 'sections_left',
        'right' => 'sections_right',
        'level' => 'sections_level'
    );
    
    $db = new SafeMySQL($dsn);
    $dbtree = new DbTreeExt($tree_params, $db);
    
    $dbtree->Delete($section_id, array(
            'and' => array(
                'section_lang = "' . $section_lang . '"'
            )
        )
    );
?>

Перемещение раздела

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

Какие этапы нам необходимо пройти?

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

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

Когда section_id перемещаемого узла и section_id нового родителя получены, действуем следующим образом:

<?php
$tree_params = array(
    'table' => 'sections',
    'id' => 'sections_id',
    'left' => 'sections_left',
    'right' => 'sections_right',
    'level' => 'sections_level'
);

$db = new SafeMySQL($dsn);
$dbtree = new DbTreeExt($tree_params, $db);

$dbtree->MoveAll($old_id, $new_id, array(
        'and' => array(
            'section_lang = "' . $section_lang . '"'
        )
    )
);
?>

Все, ваш раздел успешно перенесен. Не забывайте обрабатывать section_full_path, задавая им новые значения с учетом изменившегося пути.

Таким же образом легко поменять позицию у двух узлов (при этом дети не перемещаются).

<?php
$tree_params = array(
    'table' => 'sections',
    'id' => 'sections_id',
    'left' => 'sections_left',
    'right' => 'sections_right',
    'level' => 'sections_level'
);

$db = new SafeMySQL($dsn);
$dbtree = new DbTreeExt($tree_params, $db);

$dbtree->ChangePosition($id1, $id2, array(
        'and' => array(
            'section_lang = "' . $section_lang . '"'
        )
    )
);
?>

Таким образом мы поменяем местами узлы с номерами $id1 и $id2 соответственно. В данном случае section_full_path пересчитывать НЕ НУЖНО, поскольку поменялся лишь порядок отображения дерева, но не пути.

Хлебные крошки

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

Давайте начнем с простого – построим навигатор по сайту. Что нам для этого понадобится? Да ничего особенного, все данные к этому моменту у нас уже должны быть. Нам нужен только номер раздела, в котором сейчас находится пользователь. По идее, вы должны были его получить, когда определяли, какую страницу показывать пользователю на основании section_full_path.

Раз все данные у нас уже есть, остается только построить навигатор и вывести его на эран в нужном месте.

<?php
$tree_params = array(
    'table' => 'sections',
    'id' => 'sections_id',
    'left' => 'sections_left',
    'right' => 'sections_right',
    'level' => 'sections_level'
);

$db = new SafeMySQL($dsn);
$dbtree = new DbTreeExt($tree_params, $db);

// Получаем всех родителей раздела в котором находится пользователь
$data = $dbtree->Parents($section_id, array(
        'section_level',
        'section_path'
    ), array(
        'and' => array(
            'section_lang = "ru"'
        )
    )
);

// Печатаем навигатор
// если добавить к названию ссылку, с использованием section_full_path,
// то пользователь сможет перемещаться по своим шагам в любом направлении
foreach ($data as $item) {
    echo $section['section_name'] . ' > ';
}

?>

Построение разных меню

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

  • Первый вариант – самый простой, мы считываем с вами все дерево и показываем его пользователю в виде «Карты сайта», отмечая, скажем, жирным шрифтом, тот узел, где пользователь в данный момент находится. Получать полное дерево сайта мы с вами уже умеем. Согласитесь, этот прием очень не гибок, особенно, если у вас слишком много разделов. Также он не совсем подходит, если вы в дереве храните не структуру сайта, а, скажем, рубрикатор товаров вашего магазина. Поэтому мы переходим ко второму варианту меню.
  • Второй вариант меню – это меню в виде приоткрытого дерева. Что представляет из себя приоткрытое дерево? Это когда в данный конкретный момент пользователь видит только цепочку, ведущую до ближайших детей раздела, в котором находится пользователь. Все остальные цепочки разделов в это время «закрыты» и представлены только своими первоначальными родителями.

Для иллюстрации приведу пример.

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

MainSection 1
    Section
        Section
        Section
    Section
MainSection 2
    Section
MainSection 3
    Section
        Section <<<<<
            Section
            Section
MainSection 4
    Section
    Section

Приоткрытое дерево для выделенного элемента будет выглядеть так:

MainSection 1
MainSection 2
MainSection 3
    Section
        Section
            Section
            Section
MainSection 4

Надеюсь, что эти примеры достаточно наглядны.

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

<?php
$tree_params = array(
    'table' => 'sections',
    'id' => 'sections_id',
    'left' => 'sections_left',
    'right' => 'sections_right',
    'level' => 'sections_level'
);

$db = new SafeMySQL($dsn);
$dbtree = new DbTreeExt($tree_params, $db);

$data = $dbtree->Ajar($section_id, array(
        'section_name',
        'section_level',
        'section_full_path'
    ), array(
        'and' => array(
            'section_lang = "' . $section['section_lang'] . '"'
        )
    )
);
?>

На основе section_level вы легко можете отобразить ветку в виде лесенки (смотрите пример построения всего дерева).

  • Третий вариант – это показать пользователю только ту часть дерева, которая относится к разделу, в котором он сейчас находится. В нашем случае (смотрите примеры деревьев выше), отот вариант покажет пользователю следующее:
        Section
            Section
            Section

Это противоположный вариант метода Parents - там мы получали всех родителей, а здесь нам необходимо получить всех детей:

<?php
$tree_params = array(
    'table' => 'sections',
    'id' => 'sections_id',
    'left' => 'sections_left',
    'right' => 'sections_right',
    'level' => 'sections_level'
);

$db = new SafeMySQL($dsn);
$dbtree = new DbTreeExt($tree_params, $db);

$data = $dbtree->Branch($section_id, array(
        'section_name',
        'section_level',
        'section_full_path'
    ), array(
        'and' => array(
            'section_lang = "' . $section['section_lang'] . '"'
        )
    )
);
?>

Полное описание класса

На данный момент библиотека состоит из двух классов:

  • DbTree - базовый класс, который содержит все необходимые методы для манипуляции с деревом;
  • DbTreeExt - расширяющий класс, который содержит методы, для разнообразных способов вывода деревьев на экран.

Соответственно, класс DbTree может прекрасно обходиться без DbTreeExt. Чтобы иметь полную мощь бибилиотеки, вам необходимо инициализировать объект класса DbTreeExt.

DbTree


__construct($fields, $db, $lang = 'en')

Конструктор класса. Принимает параметрами массив со списком служебных полей дерева: id, level, left, right и название таблицы table.

$db - объект safemysql.class.php для работы с базой.

$lang - двухбуквенный код языка, на котром будут выдаваться сообщения об ошибках.


GetNode($nodeId, $fields = '*')

Возвращает все данные узла с id $nodeId. Чтобы получить список определенных полей, вам необходимо перечислить из через запятую в параметре $fields.


GetParent($nodeId, $fields = '*', $condition = '')

Возвращает данные ближайшего родителя узла с id $nodeIdЧтобы получить список определенных полей, вам необходимо перечислить из через запятую в параметре $fields.

Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


Insert($parentId, $data = array(), $condition = '')

Вставляет узел в дерево. $paretnId - id родительского узла, к которому добавляется ребенок. $data - массив данных для узла. Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


InsertNear($nodeId, $data = array(), $condition = '')

Если вам необходимо определить порядок следования детей после вставки, вы можете воспользоваться этим методом. $nodeId определяет элемент после которого будет вставлен новый, с тем же уровнем вложенности. $data - массив данных для узла. Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


MoveAll($nodeId, $parentId, $condition = '')

Перемещает ноду вместе в детьми к новому родителю. $nodeId - id перемещаемого узла, $parentId - id нового родителя. Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().

Обратите внимание: новый родитель должен быть ВНЕ переносимой ветки.


ChangePosition($nodeId1, $nodeId2)

Меняет ноды местами. То есть позволяет изменить порядок следования нод в рамках одного родителя и одного уровня. $nodeId1 и $nodeId2 - id узлов, которые меняем местами.


ChangePositionAll($nodeId1, $nodeId2, $position = 'after', $condition = '')

Меняет порядок детей у одного родителя в рамках одного уровня. Все дети переносимого элемента также перемещаются вместе с ним, сохраняя иерархию. $nodeId1 - уникальный номер перемещаемого узла (все его дети будут перемещены вместе с ним), $nodeId2 уникальный номер элемента, относительно которого будет происходить перемещение. $position - позиция, в которую будет помещен переносимый элемент ($nodeId1) относительно другого элемента ($nodeId2): 'after' - переносимый элемент ($nodeId1) будет поставлен после указанного элемента ($nodeId2), 'before' - переносимый элемент ($nodeId1) будет поставлен перед указанным ($nodeId2). Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


Delete($nodeId, $condition = '')

Удаляет ноду $nodeId. Все дети ноды останутся, переместясь на один уровень вверх.

Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


DeleteAll($nodeId, $condition = '')

Удаляет ноду $nodeId и всех ее детей.

Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


PrepareCondition($condition, $where = false, $prefix = '')

Эта функция не предназначена для прямого вызова и вызывается другими методами класса. В результате ее работы будет возвращена строка условия для запроса.

$condition – собственно требующиеся дополнительные условия. Формат следующий: array('and' => array('id = 0', 'id2 >= 3'), 'or' => array('sec = 'www'', 'sec2 <> 'erere'')), и т.д., где ключ массива – это условие (AND, OR, etc), значение ключа – непосредственно условие (user_id = 1).

$where – указывает – требуется ли добавление в начало условия ключевого слова WHERE.

$prefix – для некоторых сложных запросов есть необходимость в псевдонимах для таблицы. Префикс задает имя псевдонима (A.user_id).


DbTreeExt


Full($fields = '*', $condition = '')

Получаем дерево полностью.

Чтобы получить список определенных полей, вам необходимо перечислить из через запятую в параметре $fields.

Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


Branch($nodeId, $fields = '*', $condition = '')

Получаем всех детей $nodeId

Чтобы получить список определенных полей, вам необходимо перечислить из через запятую в параметре $fields.

Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


Parents($nodeId, $fields = '*', $condition = '')

Получаем всех родителей $nodeId.

Чтобы получить список определенных полей, вам необходимо перечислить из через запятую в параметре $fields.

Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


Ajar($nodeId, $fields = '*', $condition = '')

Возвращает приоткрытое дерево для $nodeId.

Чтобы получить список определенных полей, вам необходимо перечислить из через запятую в параметре $fields.

Дополнительные ограничительные параметры можно передать в $condition (формат параметра смотрите в описании метода PrepareCondition().


SortChildren($parentId, $orderField)

Сортирует детей $parentId по алфавиту по полю, указанному в $orderField.


MakeUlList($tree, $nameField, $linkField = array(), $linkPrefix = null, $delimiter = '')

Строит HTML дерево UL/LI. Добавляет ссылку <a href> (если нужно).

$tree - массив дерева формата nested sets (получается в результате работы любого метода библиотеки, возвращающего дерево, например, метода Full).

$nameField - массив полей, на основе которого сформируется ссылка

$linkField - название поля, в котором содержится URL ссылки.

$linkPrefix - дополнительная часть URL (добавляется в начале каждой ссылки).

$delimiter - разделяющий символ, этим значением будут разделены данные из массива $linkField

Дереву присваивается ID, состоящее из названи таблицы + _tree (<ul id="tablename_tree">).

Последние два параметра являются необязательными. Если они опущены, ссылка не формируется.