- Поддержка
lazychaser/laravel-nestedset
различными версиями Laravel - Теория
- Документация
lazychaser/laravel-nestedset
Поддержка lazychaser/laravel-nestedset
различными версиями Laravel
lazychaser/laravel-nestedset
— это пакет Laravel 4-10 для работы с деревьями в реляционных базах данных.
- Laravel 11.0 поддерживается с версии v6.0.4
- Laravel 10.0 поддерживается с версии v6.0.2
- Laravel 9.0 поддерживается с версии v6.0.1
- Laravel 8.0 поддерживается с v6.0.0
- Laravel 5.7, 5.8, 6.0, 7.0 поддерживается с v5
- Laravel 5.5, 5.6 поддерживается с v4.3
- Laravel 5.2, 5.3, 5.4 поддерживается с v4
- Laravel 5.1 поддерживается в v3
- Laravel 4 поддерживается в v2
Теория.
Что такое nested sets?
Nested sets или Nested Set Model — это способ эффективного хранения иерархических данных в реляционной таблице. Из Википедии:
Модель вложенных множеств заключается в нумерации узлов в соответствии с обходом дерева, который проходит по каждому узлу дважды, присваивая номера в порядке следования и при обоих проходах. В результате для каждого узла сохраняются два числа, которые хранятся как два атрибута. Запрос становится более простым: принадлежность к иерархии можно проверить, сравнив эти числа. Обновление требует изменения нумерации и поэтому является более затратным.
Приложения.
Модель вложенных множеств показывает хорошую производительность, когда дерево обновляется редко. Она настроена на быстрое получение связанных узлов. Она идеально подходит для построения многоуровневых меню или категорий для сайта и/или интернет-магазина.
Документация lazychaser/laravel-nestedset
Предположим, у нас есть модель Category
; переменная $node
— это экземпляр этой модели и узел, которым мы манипулируем. Это может быть как новая модель, так и модель из базы данных.
Отношения
Узел имеет следующие отношения, которые полностью функциональны и могут быть быстро загружены:
- Узел принадлежит родителю
parent
- Узел имеет много детей
children
- Узел имеет много предков
ancestors
- Узел имеет много потомков
descendants
Вставка узлов
Перемещение и вставка узлов включает несколько запросов к базе данных, поэтому настоятельно рекомендуется использовать транзакции.
ВАЖНО! Начиная с версии 4.2.0 транзакция не запускается автоматически
Еще одно важное замечание: структурные манипуляции откладываются до тех пор, не произойдет вызова save
в модели (некоторые методы неявно вызывают save
и возвращают boolean-результат операции).
Если модель успешно сохранена, это не означает, что узел был перемещен. Если ваше приложение зависит от того, действительно ли узел изменил свое положение, используйте метод hasMoved
:
if ($node->save()) {
$moved = $node->hasMoved();
}
Создание узлов
Когда узел просто создается, он добавляется в конец дерева:
Category::create($attributes); // Saved as root
$node = new Category($attributes);
$node->save(); // Saved as root
В этом случае узел считается корневым, что означает, что у него нет родителя.
Создание корня из существующего узла
// #1 Неявное сохранение
$node->saveAsRoot();
// #2 Явное сохранение
$node->makeRoot()->save();
Этот узел будет добавлен в конец дерева.
Присоединение и добавление узла к указанному родителю
Если требуется сделать узел дочерним по отношению к другому узлу, можно сделать его последним или первым дочерним.
В следующих примерах $parent - это некоторый существующий узел.
Существует несколько способов добавления узла (Append
вставляет контент в конец отобранных элементов.):
// #1 Использование отложенной вставки
$node->appendToNode($parent)->save();
// #2 Использование родительского узла
$parent->appendNode($node);
// #3 Использование отношений между родителями и детьми
$parent->children()->create($attributes);
// #5 Использование родительских отношений узла
$node->parent()->associate($parent)->save();
// #6 Использование атрибута родителя
$node->parent_id = $parent->id;
$node->save();
// #7 Использование статического метода
Category::create($attributes, $parent);
И только несколько способов добавления (Prepend
вставляет контент в начало отобранных элементов.):
// #1
$node->prependToNode($parent)->save();
// #2
$parent->prependNode($node);
Вставка перед или после указанного узла
Чтобы сделать узел $node
соседом узла $neighbor
, можно использовать следующие методы:
$neighbor
должен существовать, целевой узел может быть новый. Если целевой узел существует, он будет перемещен на новую позицию, а родитель будет изменен, если это необходимо.
# Явное сохранение
$node->afterNode($neighbor)->save();
$node->beforeNode($neighbor)->save();
# Неявное сохранение
$node->insertAfterNode($neighbor);
$node->insertBeforeNode($neighbor);
Построение дерева из массива
При использовании статического метода create
на узле проверяется, содержит ли атрибуты ключ children
. Если да, то рекурсивно создаются дополнительные узлы.
$node = Category::create([
'name' => 'Foo',
'children' => [
[
'name' => 'Bar',
'children' => [
[ 'name' => 'Baz' ],
],
],
],
]);
Теперь $node->children
содержит список созданных дочерних узлов.
Пересоздание дерева из массива
Дерево можно легко перестроить. Это удобно при массовом изменении структуры дерева.
Category::rebuildTree($data, $delete);
$data
— это массив узлов:
$data = [
[ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ],
[ 'name' => 'bar' ],
];
Для узла с именем foo
указан id
, что означает, что существующий узел будет перезаписан и сохранен. Если узел не существует, будет выброшено исключение ModelNotFoundException
. Кроме того, у этого узла указаны дочерние узлы children
, которые также являются массивом узлов; они будут обработаны таким же образом и сохранены как дочерние узлы узла foo
.
У узла bar
не указан первичный ключ, поэтому он будет создан.
$delete
показывает, нужно ли удалять узлы, которые уже существуют, но не присутствуют в $data
. По умолчанию узлы не удаляются.
Перестроение поддерева
Начиная с версии 4.2.8 возможно перестраивать поддерево:
Category::rebuildSubtree($root, $data);
Это ограничивает перестройку дерева потомками корневого узла $root
.
Получение узлов
В некоторых случаях будет использоваться переменная $id
, которая является идентификатором целевого узла.
Предки и потомки
Предки создают цепочку родителей узла. Это может пригодиться для отображения хлебных крошек к текущей категории.
Потомки — это все узлы в поддереве, т.е. дети узла, дети детей и т. д.
И предки, и потомки могут быть жадно загружены.
// Доступ к предкам
$node->ancestors;
// Доступ к потомкам
$node->descendants;
Можно загружать предков и потомков с помощью пользовательского запроса:
$result = Category::ancestorsOf($id);
$result = Category::ancestorsAndSelf($id);
$result = Category::descendantsOf($id);
$result = Category::descendantsAndSelf($id);
В большинстве случаев требуется, чтобы предки были упорядочены по уровню level:
$result = Category::defaultOrder()->ancestorsOf($id);
Коллекция предков может быть жадно загружена:
$categories = Category::with('ancestors')->paginate(30);
// во вьюхе для хлебных крошек:
@foreach($categories as $i => $category)
<small>{{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' }}</small><br>
{{ $category->name }}
@endforeach
Одноуровневые узлы
Дочерние элементы — это узлы, у которых один родитель.
$result = $node->getSiblings();
$result = $node->siblings()->get();
Чтобы получить только соседние родственные узлы:
// Получение дочернего узла, который находится сразу после данного узла
$result = $node->getNextSibling();
// Получите всех родственников, которые находятся после данного узла
$result = $node->getNextSiblings();
// Получение всех родственников с помощью запроса
$result = $node->nextSiblings()->get();
Чтобы получить предыдущие родственные узлы:
// Получение дочернего узла, который находится непосредственно перед этим узлом
$result = $node->getPrevSibling();
// Получение всех родственников, которые находятся перед узлом
$result = $node->getPrevSiblings();
// Получение всех родственников с помощью запроса
$result = $node->prevSiblings()->get();
Получение связанных моделей из другой таблицы
Представьте, что в каждой категории есть множество товаров. То есть установлено отношение HasMany
. Как получить все товары из $category
и всех ее потомков? Легко!
// Получить id потомков
$categories = $category->descendants()->pluck('id');
// Включая id самой категории
$categories[] = $category->getKey();
// Получить товары
$goods = Goods::whereIn('category_id', $categories)->get();
Получение узла вместе с его веткой потомков
Если требуется узнать, на каком уровне находится узел:
$result = Category::withDepth()->find($id);
$depth = $result->depth;
Корневой узел будет находиться на уровне 0
. Дети корневых узлов будут иметь уровень 1
и т.д.
Чтобы получить узлы указанного уровня, можно применить ограничение having
:
$result = Category::withDepth()->having('depth', '=', 1)->get();
ВАЖНО! Это не будет работать в strict mode базы данных
Упорядочивание узлов
Все узлы строго упорядочены внутри дерева. По умолчанию сортировка не применяется, поэтому узлы могут отображаться в произвольном порядке, что не влияет на отображение дерева. Порядок узлов можно задать по алфавиту или другому указателю.
Но в некоторых случаях иерархический порядок необходим. Он необходим для поиска предков и может использоваться для упорядочивания пунктов меню.
Для применения упорядочения дерева используется метод defaultOrder
:
$result = Category::defaultOrder()->get();
Узлы можно получить в обратном порядке:
$result = Category::reversed()->get();
Для перемещения узла вверх или вниз внутри родительского узла, чтобы повлиять на порядок по умолчанию:
$bool = $node->down();
$bool = $node->up();
// Сдвиг узла на 3 родственных элемента после
$bool = $node->down(3);
Результатом операции является булево значение, показывающее, изменил ли узел свою позицию.
Ограничения
Различные ограничения, которые могут быть применены к построителю запросов:
whereIsRoot()
для получения только корневых узлов;hasParent()
для получения некорневых узлов;whereIsLeaf()
для получения только веток;hasChildren()
для получения узлов, не имеющих веток;whereIsAfter($id)
для получения всех узлов (не только родственников), которые находятся после узла с указанным id;whereIsBefore($id)
для получения каждого узла, который находится перед узлом с указанным id.
Ограничения потомков:
$result = Category::whereDescendantOf($node)->get();
$result = Category::whereNotDescendantOf($node)->get();
$result = Category::orWhereDescendantOf($node)->get();
$result = Category::orWhereNotDescendantOf($node)->get();
$result = Category::whereDescendantAndSelf($id)->get();
// Включить выбранный узел в итоговый результат
$result = Category::whereDescendantOrSelf($node)->get();
Ограничения по предкам:
$result = Category::whereAncestorOf($node)->get();
$result = Category::whereAncestorOrSelf($id)->get();
$node
может быть либо первичным ключом модели, либо экземпляром модели.
Построение дерева
Получив набор узлов, его можно преобразовать в дерево. Например:
$tree = Category::get()->toTree();
Это заполнит parent
и children
отношения для каждого узла в наборе, и тогда получится дерево с использованием рекурсивного алгоритма:
$nodes = Category::get()->toTree();
$traverse = function ($categories, $prefix = '-') use (&$traverse) {
foreach ($categories as $category) {
echo PHP_EOL.$prefix.' '.$category->name;
$traverse($category->children, $prefix.'-');
}
};
$traverse($nodes);
В результате получится что-то вроде этого:
- Root
-- Child 1
--- Sub child 1
-- Child 2
- Another root
Построение плоского дерева
Также имеется возможность построить плоское дерево: список узлов, в котором дочерние узлы располагаются сразу после родительского узла. Это удобно, когда узлы располагаются в произвольном порядке (например, в алфавитном) и нет необходимости использовать рекурсию для итерации по узлам.
$nodes = Category::get()->toFlatTree();
Предыдущий пример выводит:
Root
Child 1
Sub child 1
Child 2
Another root
Получение поддерева
Иногда не нужно загружать всё дерево, а достаточно загрузить поддерево определённого узла. Это показано в следующем примере:
$root = Category::descendantsAndSelf($rootId)->toTree()->first();
В одном запросе получаем корень поддерева и всех его потомков, доступных через отношение children
.
Если сам узел $root
не нужен, сделайте следующее:
$tree = Category::descendantsOf($rootId)->toTree($rootId);
Удаление узлов
Для удаления узла:
$node->delete();
ВАЖНО! Все потомки, которые есть у этого узла, также будут удалены!
ВАЖНО! Узлы должны удаляться как модели, не пытайтесь удалить их с помощью запроса, например, так:
Category::where('id', '=', $id)->delete();
Это приведет к поломке дерева!
Поддерживается функция SoftDeletes
, в том числе на уровне модели.
Методы-хелперы
Чтобы проверить, является ли узел потомком другого узла:
$bool = $node->isDescendantOf($parent);
Чтобы проверить, является ли узел корнем:
$bool = $node->isRoot();
Прочие проверки:
$node->isChildOf($other);
$node->isAncestorOf($other);
$node->isSiblingOf($other);
$node->isLeaf()
Проверка целостности
Проверить, не нарушено ли дерево (т.е. имеет ли оно структурные ошибки):
$bool = Category::isBroken();
Можно получить статистику ошибок:
$data = Category::countErrors();
Возвращается массив со следующими ключами:
oddness
— количество узлов, имеющих неправильный набор значенийlft
иrgt
duplicates
— количество узлов, имеющих одинаковые значенияlft
иrgt
wrong_parent
— количество узлов с неверным значениемparent_id
, не соответствующим значениямlft
иrgt
missing_parent
— количество узлов, у которыхparent_id
указывает на несуществующий узел
Исправление дерева
С версии 3.1 можно исправить дерево. Используя информацию о наследовании из колонки parent_id
, для каждого узла устанавливаются правильные значения _lft
и _rgt
.
Node::fixTree();
Рубрикатор (Scoping)
Допустим, есть модель Menu
и MenuItems
. Между этими моделями установлено отношение «один-ко-многим». MenuItem
имеет атрибут menu_id
для nested sets. MenuItem
включает в себя nested sets. Очевидно, что потребуется обрабатывать каждое дерево отдельно на основе атрибута menu_id
. Для этого необходимо указать этот атрибут в качестве scope атрибута:
protected function getScopeAttributes()
{
return [ 'menu_id' ];
}
Но теперь, чтобы выполнить некоторый пользовательский запрос, требуется предоставить атрибуты, которые используются для определения scope-параметров:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OK
MenuItem::descendantsOf($id)->get(); // НЕВЕРНО: возвращает узлы из другого диапазона scope
MenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); // OK
При запросе узлов с использованием экземпляра модели диапазоны scope применяются автоматически, основываясь на атрибутах этой модели:
$node = MenuItem::findOrFail($id);
$node->siblings()->withDepth()->get(); // OK
Чтобы получить экземпляр построителя запросов scope, используйте экземпляр:
$node->newScopedQuery();
Скопинг и жадная загрузка
При жадной загрузке всегда используйте скопированные запросы:
MenuItem::scoped([ 'menu_id' => 5])->with('descendants')->findOrFail($id); // OK
MenuItem::with('descendants')->findOrFail($id); // НЕПРАВИЛЬНО
Технические требования
- PHP >= 5.4
- Laravel >= 4.1
Настоятельно рекомендуется использовать базу данных, поддерживающую транзакции (например, InnoDb от MySql), чтобы защитить дерево от возможного повреждения.
Установка
Чтобы установить пакет, запустите в консоли:
composer require kalnoy/nestedset
Установка с нуля
Миграции
Для пользователей Laravel 5.5 и выше:
Schema::create('table', function (Blueprint $table) {
...
$table->nestedSet();
});
// To drop columns
Schema::table('table', function (Blueprint $table) {
$table->dropNestedSet();
});
Для предыдущих версий Laravel:
...
use Kalnoy\Nestedset\NestedSet;
Schema::create('table', function (Blueprint $table) {
...
NestedSet::columns($table);
});
Чтобы удалить столбцы:
...
use Kalnoy\Nestedset\NestedSet;
Schema::table('table', function (Blueprint $table) {
NestedSet::dropColumns($table);
});
Модель
Для включения nested sets модель должна использовать трейт Kalnoy\Nestedset\NodeTrait
:
use Kalnoy\Nestedset\NodeTrait;
class Foo extends Model {
use NodeTrait;
}
Миграция существующих данных
Миграция другого расширения с nested set
Если ваше предыдущее расширение использовало другой набор колонок, нужно просто переопределить следующие методы в классе модели:
public function getLftName()
{
return 'left';
}
public function getRgtName()
{
return 'right';
}
public function getParentIdName()
{
return 'parent';
}
// Задание мутатора родительского атрибута id
public function setParentAttribute($value)
{
$this->setParentIdAttribute($value);
}
Миграция с базовой информации о родителях
Если используемое дерево содержит информацию о parent_id, необходимо добавить в схему два столбца:
$table->unsignedInteger('_lft');
$table->unsignedInteger('_rgt');
После настройки модели остается только подправить дерево, чтобы заполнить столбцы _lft
и _rgt
:
MyModel::fixTree();
Лицензия
Copyright (c) 2017 Александр Калной
Настоящим предоставляется бесплатное разрешение любому лицу, получившему копию данного программного обеспечения и связанных с ним файлов документации («Программное обеспечение»), совершать неограниченные сделки с Программным обеспечением, включая, без ограничения, права на использование, копирование, модификацию, комбинирование, публикацию, распространение, сублицензирование и/или продажу копий Программного обеспечения, а также разрешать это делать лицам, которым предоставляется Программное обеспечение, при соблюдении следующих условий:
Вышеуказанное уведомление об авторских правах и данное уведомление о разрешении должны быть включены во все копии или существенные части Программного обеспечения.
ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ, ГАРАНТИЯМИ ТОВАРНОГО СОСТОЯНИЯ, ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ И НЕНАРУШЕНИЯ ПРАВ. НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА ЛЮБЫЕ ПРЕТЕНЗИИ, УБЫТКИ ИЛИ ДРУГИЕ ОБЯЗАТЕЛЬСТВА, БУДЬ ТО ДОГОВОРНЫЕ, ГРАЖДАНСКО-ПРАВОВЫЕ ИЛИ ИНЫЕ, ВОЗНИКАЮЩИЕ ИЗ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ, ЕГО ИСПОЛЬЗОВАНИЯ ИЛИ ДРУГИХ ОПЕРАЦИЙ С НИМ ИЛИ В СВЯЗИ С НИМИ.
Перевод с английского официальной документации:
https://github.com/lazychaser/laravel-nestedset
Заберите ссылку на статью к себе, чтобы потом легко её найти!
Раз уж досюда дочитали, то может может есть желание рассказать об этом месте своим друзьям, знакомым и просто мимо проходящим?
Не надо себя сдерживать! ;)