Это пакет Laravel 4-10 для работы с деревьями в реляционных базах данных.
Laravel 11.0 поддерживается начиная с версии 6.0.4.
Laravel 10.0 поддерживается начиная с версии 6.0.2.
Laravel 9.0 поддерживается начиная с версии 6.0.1.
Laravel 8.0 поддерживается начиная с версии 6.0.0.
Laravel 5.7, 5.8, 6.0, 7.0 поддерживается начиная с версии 5.
Laravel 5.5, 5.6 поддерживается начиная с версии 4.3.
Laravel 5.2, 5.3, 5.4 поддерживается начиная с версии 4.
Laravel 5.1 поддерживается в версии 3.
Laravel 4 поддерживается в версии 2.
Содержание:
Теория
Документация
Вставка узлов
Получение узлов
Удаление узлов
Проверка и исправление согласованности
Обзор
Требования
Установка
Вложенные наборы или модель вложенных наборов — это способ эффективного хранения иерархических данных в реляционной таблице. Из Википедии:
Модель вложенного множества заключается в нумерации узлов в соответствии с обходом дерева, при котором каждый узел посещается дважды, присваивая номера в порядке посещения и при обоих посещениях. В результате для каждого узла остается два числа, которые сохраняются как два атрибута. Запросы становятся недорогими: членство в иерархии можно проверить путем сравнения этих чисел. Обновление требует изменения нумерации и поэтому является дорогостоящим.
NSM показывает хорошую производительность, когда дерево обновляется редко. Он настроен на быстрое получение связанных узлов. Он идеально подходит для создания многоуровневого меню или категорий для магазина.
Предположим, что у нас есть модель Category
; переменная $node
— это экземпляр этой модели и узла, которым мы манипулируем. Это может быть свежая модель или модель из базы данных.
Узел имеет следующие связи, которые полностью функциональны и могут быть легко загружены:
Узел принадлежит parent
У узла много children
У узла много ancestors
У узла много descendants
Перемещение и вставка узлов включает в себя несколько запросов к базе данных, поэтому настоятельно рекомендуется использовать транзакции.
ВАЖНЫЙ! Начиная с версии 4.2.0 транзакция не запускается автоматически.
Еще одно важное замечание: структурные манипуляции откладываются до тех пор, пока вы не нажмете save
модель (некоторые методы неявно вызывают save
и возвращают логический результат операции).
Если модель успешно сохранена, это не означает, что узел был перемещен. Если ваше приложение зависит от того, действительно ли узел изменил свою позицию, используйте метод hasMoved
:
if ($node->save()) {$moved = $node->hasMoved(); }
Когда вы просто создаете узел, он будет добавлен в конец дерева:
Категория::создать($атрибуты); // Сохраняется как root
$node = новая категория($attributes);$node->save(); // Сохраняется как root
В этом случае узел считается корнем , что означает, что у него нет родителя.
// #1 Неявное save$node->saveAsRoot(); // #2 Явное save$node->makeRoot()->save();
Узел будет добавлен в конец дерева.
Если вы хотите сделать узел дочерним по отношению к другому узлу, вы можете сделать его последним или первым дочерним элементом.
В следующих примерах $parent
— это какой-то существующий узел.
Есть несколько способов добавить узел:
// #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);
И только пара способов добавить:
// #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 ($ сосед);
При использовании статического метода create
на узле он проверяет, содержат ли атрибуты children
ключ. Если это так, он рекурсивно создает больше узлов.
$node = Category::create(['name' => 'Foo','children' => [ ['name' => 'Бар','дети' => [ [ 'имя' => 'Баз' ], ], ], ], ]);
$node->children
теперь содержит список созданных дочерних узлов.
Вы можете легко восстановить дерево. Это полезно для массового изменения структуры дерева.
Категория::rebuildTree($data, $delete);
$data
— это массив узлов:
$данные = [ [ 'id' => 1, 'name' => 'foo', 'дети' => [ ... ] ], [ 'имя' => 'бар' ], ];
Для узла указан идентификатор с именем foo
, что означает, что существующий узел будет заполнен и сохранен. Если узел не существует, выдается исключение ModelNotFoundException
. Кроме того, для этого узла указаны children
, которые также являются массивом узлов; они будут обработаны таким же образом и сохранены как дочерние элементы узла foo
.
bar
узла не указан первичный ключ, поэтому он будет создан.
$delete
показывает, следует ли удалять узлы, которые уже существуют, но отсутствуют в $data
. По умолчанию узлы не удаляются.
Начиная с версии 4.2.8 вы можете перестроить поддерево:
Категория::rebuildSubtree($root, $data);
Это ограничивает перестроение дерева для потомков узла $root
.
В некоторых случаях мы будем использовать переменную $id
, которая является идентификатором целевого узла.
Предки составляют цепочку родителей к узлу. Полезно для отображения навигационной навигации по текущей категории.
Потомками являются все узлы в поддереве, т.е. дочерние элементы узла, дочерние элементы дочерних элементов и т. д.
И предки, и потомки могут быть загружены с готовностью.
// Доступ к предкам$node->ancestors;// Доступ к потомкам$node->descendants;
Предков и потомков можно загрузить с помощью специального запроса:
$result = Категория::ancestorsOf($id);$result = Категория::ancestorsAndSelf($id);$result = Категория::descendantsOf($id);$result = Категория::descendantsAndSelf($id);
В большинстве случаев вам нужно, чтобы ваши предки были упорядочены по уровням:
$result = Категория::defaultOrder()->ancestorsOf($id);
Коллекцию предков можно с готовностью загрузить:
$categories = Category::with('ancestors')->paginate(30);// в поле зрения для хлебных крошек:@foreach($categories as $i => $category) <small>{{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Верхний уровень' }}</small><br> {{ $category->имя }} @endforeach
Братья и сестры — это узлы, имеющие одного и того же родителя.
$result = $node->getSiblings();$result = $node->siblings()->get();
Чтобы получить только следующих братьев и сестер:
// Получить одноуровневый элемент, который находится сразу после узла $result = $node->getNextSibling(); // Получить всех одноуровневых элементов, которые находятся после узла $result = $node->getNextSiblings(); // Получить всех одноуровневых элементов, используя query$result = $node->nextSiblings()->get();
Чтобы получить предыдущих братьев и сестер:
// Получить одноуровневый элемент, который находится непосредственно перед узлом $result = $node->getPrevSibling(); // Получить всех одноуровневых элементов, которые находятся перед узлом $result = $node->getPrevSiblings(); // Получить всех одноуровневых элементов, используя query$result = $node->prevSiblings()->get();
Представьте, что has many
товаров. Т.е. связь HasMany
установлена. Как получить все товары $category
и каждого ее потомка? Легкий!
// Получаем идентификаторы потомков$categories = $category->descendants()->pluck('id');// Включаем идентификатор самой категории$categories[] = $category->getKey();// Получаем товары $goods = Товары::whereIn('category_id', $categories)->get();
Если вам нужно знать, на каком уровне находится узел:
$result = Категория::withDepth()->find($id);$глубина = $result->глубина;
Корневой узел будет иметь уровень 0. Дочерние узлы корневых узлов будут иметь уровень 1 и т. д.
Чтобы получить узлы указанного уровня, вы можете применить having
:
$result = Категория::withDepth()->having('глубина', '=', 1)->get();
ВАЖНЫЙ! Это не будет работать в строгом режиме базы данных.
Все узлы строго организованы внутри. По умолчанию порядок не применяется, поэтому узлы могут появляться в случайном порядке, и это не влияет на отображение дерева. Вы можете упорядочить узлы по алфавиту или другому индексу.
Но в некоторых случаях иерархический порядок необходим. Он необходим для получения предков и может использоваться для упорядочивания пунктов меню.
Для применения порядка дерева используется метод defaultOrder
:
$result = Категория::defaultOrder()->get();
Вы можете получить узлы в обратном порядке:
$result = Категория::reversed()->get();
Чтобы сдвинуть узел вверх или вниз внутри родителя, чтобы изменить порядок по умолчанию:
$bool = $node->down();$bool = $node->up();// Сдвиг узла на 3 узла $bool = $node->down(3);
Результатом операции является логическое значение того, изменил ли узел свое положение.
Различные ограничения, которые можно применить к построителю запросов:
whereIsRoot(), чтобы получить только корневые узлы;
hasParent() для получения некорневых узлов;
whereIsLeaf(), чтобы получить только листья;
hasChildren() для получения невыходящих узлов;
whereIsAfter($id) для получения каждого узла (а не только одноуровневых), находящихся после узла с указанным идентификатором;
whereIsBefore($id) для получения каждого узла, находящегося перед узлом с указанным идентификатором.
Ограничения потомков:
$result = Категория::whereDescendantOf($node)->get();$result = Категория::whereNotDescendantOf($node)->get();$result = Категория::orWhereDescendantOf($node)->get() ;$result = Категория::orWhereNotDescendantOf($node)->get();$result = Категория::whereDescendantAndSelf($id)->get();// Включаем целевой узел в набор результатов $result = Категория::whereDescendantOrSelf($node)->get();
Ограничения предков:
$result = Категория::whereAncestorOf($node)->get();$result = Категория::whereAncestorOrSelf($id)->get();
$node
может быть либо первичным ключом модели, либо экземпляром модели.
Получив набор узлов, вы можете преобразовать его в дерево. Например:
$tree = Категория::get()->toTree();
Это заполнит parent
и children
отношения на каждом узле набора, и вы сможете визуализировать дерево, используя рекурсивный алгоритм:
$nodes = Category::get()->toTree();$traverse = function ($categories, $prefix = '-') use (&$traverse) {foreach ($categories as $category) {echo PHP_EOL.$ префикс.' '.$category->name;$traverse($category->children, $prefix.'-'); } };$traverse($nodes);
Это выведет что-то вроде этого:
- Root -- Child 1 --- Sub child 1 -- Child 2 - Another root
Также вы можете построить плоское дерево: список узлов, в котором дочерние узлы находятся сразу после родительского узла. Это полезно, когда вы получаете узлы в произвольном порядке (т. е. в алфавитном порядке) и не хотите использовать рекурсию для перебора узлов.
$nodes = Категория::get()->toFlatTree();
Предыдущий пример выведет:
Root Child 1 Sub child 1 Child 2 Another root
Иногда вам не нужно загружать все дерево, а только какое-то поддерево определенного узла. Это показано в следующем примере:
$root = Категория::descendantsAndSelf($rootId)->toTree()->first();
В одном запросе мы получаем корень поддерева и всех его потомков, доступных через children
отношение.
Если вам не нужен сам узел $root
, сделайте следующее:
$tree = Категория::descendantsOf($rootId)->toTree($rootId);
Чтобы удалить узел:
$узел->удалить();
ВАЖНЫЙ! Любой потомок этого узла также будет удален!
ВАЖНЫЙ! Узлы необходимо удалять как модели, не пытайтесь удалить их с помощью такого запроса:
Категория::where('id', '=', $id)->delete();
Это сломает дерево!
Поддерживается черта SoftDeletes
, в том числе на уровне модели.
Чтобы проверить, является ли узел потомком другого узла:
$bool = $node->isDescendantOf($parent);
Чтобы проверить, является ли узел корневым:
$bool = $node->isRoot();
Другие проверки:
$node->isChildOf($other);
$node->isAncestorOf($other);
$node->isSiblingOf($other);
$node->isLeaf()
Вы можете проверить, сломано ли дерево (т.е. имеет ли оно структурные ошибки):
$bool = Категория::isBroken();
Статистику ошибок можно получить:
$data = Категория::countErrors();
Он вернет массив со следующими ключами:
oddness
— количество узлов с неправильным набором значений lft
и rgt
duplicates
— количество узлов с одинаковыми значениями lft
или rgt
wrong_parent
— количество узлов с недопустимым значением parent_id
, которое не соответствует значениям lft
и rgt
.
missing_parent
— количество узлов, у которых parent_id
указывает на несуществующий узел.
Так как дерево v3.1 теперь можно исправить. Используя информацию о наследовании из parent_id
, для каждого узла устанавливаются правильные значения _lft
и _rgt
.
Узел::fixTree();
Представьте, что у вас есть модель Menu
и MenuItems
. Между этими моделями установлена связь «один ко многим». MenuItem
имеет атрибут menu_id
для объединения моделей. MenuItem
включает вложенные наборы. Очевидно, что вы захотите обрабатывать каждое дерево отдельно на основе атрибута menu_id
. Для этого вам необходимо указать этот атрибут как атрибут области:
защищенная функция getScopeAttributes() {return ['menu_id']; }
Но теперь, чтобы выполнить какой-либо специальный запрос, вам необходимо предоставить атрибуты, которые используются для определения области действия:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OKMenuItem::descendantsOf($id)->get(); // НЕПРАВИЛЬНО: возвращает узлы из другого объектаscopeMenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); // ХОРОШО
При запросе узлов с использованием экземпляра модели области действия применяются автоматически на основе атрибутов этой модели:
$node = MenuItem::findOrFail($id);$node->siblings()->withDepth()->get(); // ХОРОШО
Чтобы получить построитель запросов с областью действия, используя экземпляр:
$node->newScopedQuery();
Всегда используйте запрос с областью действия при быстрой загрузке:
MenuItem::scoped([ 'menu_id' => 5])->with('потомки')->findOrFail($id); // OKMenuItem::with('descendants')->findOrFail($id); // НЕПРАВИЛЬНЫЙ
PHP >= 5.4
Ларавел >= 4.1
Настоятельно рекомендуется использовать базу данных, поддерживающую транзакции (например, InnoDb MySql), чтобы защитить дерево от возможного повреждения.
Чтобы установить пакет, в терминале:
composer require kalnoy/nestedset
Для пользователей Laravel 5.5 и выше:
Schema::create('table', function (Blueprint $table) {...$table->nestedSet(); });// Чтобы удалить columnsSchema::table('table', function (Blueprint $table) {$table->dropNestedSet(); });
Для предыдущих версий Laravel:
... использовать KalnoyNestedsetNestedSet; Schema::create('table', function (Blueprint $table) {...NestedSet::columns($table); });
Чтобы удалить столбцы:
... использовать KalnoyNestedsetNestedSet; Schema::table('table', function (Blueprint $table) { NestedSet::dropColumns($table); });
Ваша модель должна использовать признак KalnoyNestedsetNodeTrait
для включения вложенных наборов:
используйте KalnoyNestedsetNodeTrait; класс Foo расширяет модель {use NodeTrait; }
Если ваше предыдущее расширение использовало другой набор столбцов, вам просто нужно переопределить следующие методы в классе модели:
публичная функция getLftName() {возврат 'влево'; }публичная функция getRgtName() {возврат 'вправо'; } Публичная функция getParentIdName() {вернуть 'родитель'; }// Укажите родительский атрибут id mutatorpublic function setParentAttribute($value) {$this->setParentIdAttribute($value); }
Если ваше дерево содержит информацию parent_id
, вам необходимо добавить в схему два столбца:
$table->unsignedInteger('_lft');$table->unsignedInteger('_rgt'); $table->unsignedInteger('_rgt');
После настройки модели вам нужно только исправить дерево, чтобы заполнить столбцы _lft
и _rgt
:
МояМодель::fixTree();
Copyright (c) 2017 Александр Кальный
Настоящим бесплатно любому лицу, получившему копию этого программного обеспечения и связанных с ним файлов документации («Программное обеспечение»), предоставляется разрешение на работу с Программным обеспечением без ограничений, включая, помимо прочего, права на использование, копирование, изменение, объединение. публиковать, распространять, сублицензировать и/или продавать копии Программного обеспечения, а также разрешать лицам, которым предоставлено Программное обеспечение, делать это при соблюдении следующих условий:
Вышеупомянутое уведомление об авторских правах и настоящее уведомление о разрешении должны быть включены во все копии или существенные части Программного обеспечения.
ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ, ГАРАНТИЯМИ ТОВАРНОЙ ЦЕННОСТИ, ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ И НЕНАРУШЕНИЯ ПРАВ. НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ АВТОРЫ ИЛИ ОБЛАДАТЕЛИ АВТОРСКИХ ПРАВ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА ЛЮБЫЕ ПРЕТЕНЗИИ, УБЫТКИ ИЛИ ДРУГУЮ ОТВЕТСТВЕННОСТЬ, БУДЬ В ДЕЙСТВИЯХ ПО КОНТРАКТУ, ПРАВОНАРУШЕНИЮ ИЛИ ДРУГИМ ОБРАЗОМ, ВОЗНИКАЮЩИЕ ОТ, ИЗ ИЛИ В СВЯЗИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ИЛИ ДРУГИМИ СДЕЛКАМИ, ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ.