diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index fbcb397..894756b 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -3,7 +3,7 @@ name: Unit Tests
on:
push:
branches:
- - master
+ - main
pull_request:
branches:
- "**"
@@ -22,13 +22,14 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: [8.0, 8.1, 8.2, 8.3, 8.4]
+ php: ['8.3', '8.4']
+ laravel: ['^12.0', '^13.0']
- name: PHP${{ matrix.php }}
+ name: PHP${{ matrix.php }} - Laravel${{ matrix.laravel }}
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -39,7 +40,8 @@ jobs:
- name: Install dependencies
run: |
- composer install -o --quiet
+ composer require "illuminate/support:${{ matrix.laravel }}" "illuminate/database:${{ matrix.laravel }}" "illuminate/events:${{ matrix.laravel }}" --no-update
+ composer update -o --quiet --prefer-dist
- name: Execute Unit Tests
run: composer test
diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
deleted file mode 100644
index dcb7bb0..0000000
--- a/CHANGELOG.markdown
+++ /dev/null
@@ -1,117 +0,0 @@
-### 4.3.4
-* Support Laravel 5.8
-
-### 4.3.3
-* Support Laravel 5.7
-
-### 4.3.2
-* Support Laravel 5.6
-* Added `nestedSet` and `dropNestedSet` blueprint macros
-
-### 4.3.0
-* Support Laravel 5.5
-* Added `fixSubtree` and `rebuildSubtree` methods
-* Increased performance of tree rebuilding
-
-### 4.2.7
-
-* #217: parent_id, lft and rgt are reset when replicating a node
-
-### 4.2.5
-
-* #208: dirty parent and bounds when making a root
-* #206: fixed where has on descendants
-* refactored ancestors and descendants relations
-
-### 4.2.4
-
-* Fixed issues related to rebuilding tree when using `SoftDeletes` and scoping
-
-### 4.2.3
-
-* Added `whereAncestorOrSelf`, `ancestorsAndSelf`, `descendantsOrSelf`,
- `whereDescendantOrSelf` helper methods
-* #186: rebuild tree removes nodes softly when model uses SoftDeletes trait
-* #191: added `whereIsLeaf` and `leaves` method, added `isLeaf` check on node
-
-### 4.1.0
-
-* #75: Converted to trait. Following methods were renamed:
- - `appendTo` to `appendToNode`
- - `prependTo` to `prependToNode`
- - `insertBefore` to `insertBeforeNode`
- - `insertAfter` to `insertAfterNode`
- - `getNext` to `getNextNode`
- - `getPrev` to `getPrevNode`
-* #82: Fixing tree now handles case when nodes pointing to non-existing parent
-* The number of missing parent is now returned when using `countErrors`
-* #79: implemented scoping feature
-* #81: moving node now makes model dirty before saving it
-* #45: descendants is now a relation that can be eagerly loaded
-* `hasChildren` and `hasParent` are now deprecated. Use `has('children')`
- `has('parent')` instead
-* Default order is no longer applied for `siblings()`, `descendants()`,
- `prevNodes`, `nextNodes`
-* #50: implemented tree rebuilding feature
-* #85: added tree flattening feature
-
-### 3.1.1
-
-* Fixed #42: model becomes dirty before save when parent is changed and using `appendTo`,
- `prependTo`, `insertBefore`, `insertAfter`.
-
-### 3.1.0
-
-* Added `fixTree` method for fixing `lft`/`rgt` values based on inheritance
-* Dropped support of Laravel < 5.1
-* Improved compatibility with different databases
-
-### 3.0.0
-
-* Support Laravel 5.1.9
-* Renamed `append` to `appendNode`, `prepend` to `prependNode`
-* Renamed `next` to `nextNodes`, `prev` to `prevNodes`
-* Renamed `after` to `afterNode`, `before` to `beforeNode`
-
-### 2.4.0
-
-* Added query methods `whereNotDescendantOf`, `orWhereDescendantOf`, `orWhereNotDescendantOf`
-* `whereAncestorOf`, `whereDescendantOf` and every method that depends on them can now accept node instance
-* Added `Node::getBounds` that returns an array of node bounds that can be used in `whereNodeBetween`
-
-### 2.3.0
-
-* Added `linkNodes` method to `Collection` class
-
-### 2.2.0
-
-* Support Laravel 5
-
-### 2.1.0
-
-* Added `isChildOf`, `isAncestorOf`, `isSiblingOf` methods
-
-### 2.0.0
-
-* Added `insertAfter`, `insertBefore` methods.
-* `prepend` and `append` methods now save target model.
-* You can now call `refreshNode` to make sure that node has updated structural
- data (lft and rgt values).
-* The root node is not required now. You can use `saveAsRoot` or `makeRoot` method.
- New model is saved as root by default.
-* You can now create as many nodes and in any order as you want within single
- request.
-* Laravel 2 is supported but not required.
-* `ancestorsOf` now doesn't include target node into results.
-* New constraint methods `hasParent` and `hasChildren`.
-* New method `isDescendantOf` that checks if node is a descendant of other node.
-* Default order is not applied by default.
-* New method `descendantsOf` that allows to get descendants by id of the node.
-* Added `countErrors` and `isBroken` methods to check whether the tree is broken.
-* `NestedSet::createRoot` has been removed.
-* `NestedSet::column` doesn't create a foreign key anymore.
-
-### 1.1.0
-
-* `Collection::toDictionary` is now obsolete. Use `Collection::groupBy`.
-* Laravel 4.2 is required
diff --git a/README.markdown b/README.markdown
deleted file mode 100644
index cf5d191..0000000
--- a/README.markdown
+++ /dev/null
@@ -1,739 +0,0 @@
-[](https://travis-ci.org/lazychaser/laravel-nestedset)
-[](https://packagist.org/packages/kalnoy/nestedset)
-[](https://packagist.org/packages/kalnoy/nestedset)
-[](https://packagist.org/packages/kalnoy/nestedset)
-[](https://packagist.org/packages/kalnoy/nestedset)
-
-This is a Laravel package for working with trees in relational databases.
-
-* **Laravel 13** is supported since v7.0.0
-* **Laravel 12** is supported since v6.0.7
-* **Laravel 11.0** is supported since v6.0.4
-* **Laravel 10.0** is supported since v6.0.2
-* **Laravel 9.0** is supported since v6.0.1
-* **Laravel 8.0** is supported since v6.0.0
-* **Laravel 5.7, 5.8, 6.0, 7.0** is supported since v5
-* **Laravel 5.5, 5.6** is supported since v4.3
-* **Laravel 5.2, 5.3, 5.4** is supported since v4
-* **Laravel 5.1** is supported in v3
-* **Laravel 4** is supported in v2
-
-__Contents:__
-
-- [Theory](#what-are-nested-sets)
-- [Documentation](#documentation)
- - [Inserting nodes](#inserting-nodes)
- - [Retrieving nodes](#retrieving-nodes)
- - [Deleting nodes](#deleting-nodes)
- - [Consistency checking & fixing](#checking-consistency)
- - [Scoping](#scoping)
-- [Requirements](#requirements)
-- [Installation](#installation)
-
-What are nested sets?
----------------------
-
-Nested sets or [Nested Set Model](http://en.wikipedia.org/wiki/Nested_set_model) is
-a way to effectively store hierarchical data in a relational table. From wikipedia:
-
-> The nested set model is to number the nodes according to a tree traversal,
-> which visits each node twice, assigning numbers in the order of visiting, and
-> at both visits. This leaves two numbers for each node, which are stored as two
-> attributes. Querying becomes inexpensive: hierarchy membership can be tested by
-> comparing these numbers. Updating requires renumbering and is therefore expensive.
-
-### Applications
-
-NSM shows good performance when tree is updated rarely. It is tuned to be fast for
-getting related nodes. It'is ideally suited for building multi-depth menu or
-categories for shop.
-
-Documentation
--------------
-
-Suppose that we have a model `Category`; a `$node` variable is an instance of that model
-and the node that we are manipulating. It can be a fresh model or one from database.
-
-### Relationships
-
-Node has following relationships that are fully functional and can be eagerly loaded:
-
-- Node belongs to `parent`
-- Node has many `children`
-- Node has many `ancestors`
-- Node has many `descendants`
-
-### Inserting nodes
-
-Moving and inserting nodes includes several database queries, so it is
-highly recommended to use transactions.
-
-__IMPORTANT!__ As of v4.2.0 transaction is not automatically started
-
-Another important note is that __structural manipulations are deferred__ until you
-hit `save` on model (some methods implicitly call `save` and return boolean result
-of the operation).
-
-If model is successfully saved it doesn't mean that node was moved. If your application
-depends on whether the node has actually changed its position, use `hasMoved` method:
-
-```php
-if ($node->save()) {
- $moved = $node->hasMoved();
-}
-```
-
-#### Creating nodes
-
-When you simply creating a node, it will be appended to the end of the tree:
-
-```php
-Category::create($attributes); // Saved as root
-```
-
-```php
-$node = new Category($attributes);
-$node->save(); // Saved as root
-```
-
-In this case the node is considered a _root_ which means that it doesn't have a parent.
-
-#### Making a root from existing node
-
-```php
-// #1 Implicit save
-$node->saveAsRoot();
-
-// #2 Explicit save
-$node->makeRoot()->save();
-```
-
-The node will be appended to the end of the tree.
-
-#### Appending and prepending to the specified parent
-
-If you want to make node a child of other node, you can make it last or first child.
-
-*In following examples, `$parent` is some existing node.*
-
-There are few ways to append a node:
-
-```php
-// #1 Using deferred insert
-$node->appendToNode($parent)->save();
-
-// #2 Using parent node
-$parent->appendNode($node);
-
-// #3 Using parent's children relationship
-$parent->children()->create($attributes);
-
-// #5 Using node's parent relationship
-$node->parent()->associate($parent)->save();
-
-// #6 Using the parent attribute
-$node->parent_id = $parent->id;
-$node->save();
-
-// #7 Using static method
-Category::create($attributes, $parent);
-```
-
-And only a couple ways to prepend:
-
-```php
-// #1
-$node->prependToNode($parent)->save();
-
-// #2
-$parent->prependNode($node);
-```
-
-#### Inserting before or after specified node
-
-You can make `$node` to be a neighbor of the `$neighbor` node using following methods:
-
-*`$neighbor` must exists, target node can be fresh. If target node exists,
-it will be moved to the new position and parent will be changed if it's required.*
-
-```php
-# Explicit save
-$node->afterNode($neighbor)->save();
-$node->beforeNode($neighbor)->save();
-
-# Implicit save
-$node->insertAfterNode($neighbor);
-$node->insertBeforeNode($neighbor);
-```
-
-#### Building a tree from array
-
-When using static method `create` on node, it checks whether attributes contains
-`children` key. If it does, it creates more nodes recursively.
-
-```php
-$node = Category::create([
- 'name' => 'Foo',
-
- 'children' => [
- [
- 'name' => 'Bar',
-
- 'children' => [
- [ 'name' => 'Baz' ],
- ],
- ],
- ],
-]);
-```
-
-`$node->children` now contains a list of created child nodes.
-
-#### Rebuilding a tree from array
-
-You can easily rebuild a tree. This is useful for mass-changing the structure of
-the tree.
-
-```php
-Category::rebuildTree($data, $delete);
-```
-
-`$data` is an array of nodes:
-
-```php
-$data = [
- [ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ],
- [ 'name' => 'bar' ],
-];
-```
-
-There is an id specified for node with the name of `foo` which means that existing
-node will be filled and saved. If node is not exists `ModelNotFoundException` is
-thrown. Also, this node has `children` specified which is also an array of nodes;
-they will be processed in the same manner and saved as children of node `foo`.
-
-Node `bar` has no primary key specified, so it will be created.
-
-`$delete` shows whether to delete nodes that are already exists but not present
-in `$data`. By default, nodes aren't deleted.
-
-##### Rebuilding a subtree
-
-As of 4.2.8 you can rebuild a subtree:
-
-```php
-Category::rebuildSubtree($root, $data);
-```
-
-This constraints tree rebuilding to descendants of `$root` node.
-
-### Retrieving nodes
-
-*In some cases we will use an `$id` variable which is an id of the target node.*
-
-#### Ancestors and descendants
-
-Ancestors make a chain of parents to the node. Helpful for displaying breadcrumbs
-to the current category.
-
-Descendants are all nodes in a sub tree, i.e. children of node, children of
-children, etc.
-
-Both ancestors and descendants can be eagerly loaded.
-
-```php
-// Accessing ancestors
-$node->ancestors;
-
-// Accessing descendants
-$node->descendants;
-```
-
-It is possible to load ancestors and descendants using custom query:
-
-```php
-$result = Category::ancestorsOf($id);
-$result = Category::ancestorsAndSelf($id);
-$result = Category::descendantsOf($id);
-$result = Category::descendantsAndSelf($id);
-```
-
-In most cases, you need your ancestors to be ordered by the level:
-
-```php
-$result = Category::defaultOrder()->ancestorsOf($id);
-```
-
-A collection of ancestors can be eagerly loaded:
-
-```php
-$categories = Category::with('ancestors')->paginate(30);
-
-// in view for breadcrumbs:
-@foreach($categories as $i => $category)
- {{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' }}
- {{ $category->name }}
-@endforeach
-```
-
-#### Siblings
-
-Siblings are nodes that have same parent.
-
-```php
-$result = $node->getSiblings();
-
-$result = $node->siblings()->get();
-```
-
-To get only next siblings:
-
-```php
-// Get a sibling that is immediately after the node
-$result = $node->getNextSibling();
-
-// Get all siblings that are after the node
-$result = $node->getNextSiblings();
-
-// Get all siblings using a query
-$result = $node->nextSiblings()->get();
-```
-
-To get previous siblings:
-
-```php
-// Get a sibling that is immediately before the node
-$result = $node->getPrevSibling();
-
-// Get all siblings that are before the node
-$result = $node->getPrevSiblings();
-
-// Get all siblings using a query
-$result = $node->prevSiblings()->get();
-```
-
-#### Getting related models from other table
-
-Imagine that each category `has many` goods. I.e. `HasMany` relationship is established.
-How can you get all goods of `$category` and every its descendant? Easy!
-
-```php
-// Get ids of descendants
-$categories = $category->descendants()->pluck('id');
-
-// Include the id of category itself
-$categories[] = $category->getKey();
-
-// Get goods
-$goods = Goods::whereIn('category_id', $categories)->get();
-```
-
-#### Including node depth
-
-If you need to know at which level the node is:
-
-```php
-$result = Category::withDepth()->find($id);
-
-$depth = $result->depth;
-```
-
-Root node will be at level 0. Children of root nodes will have a level of 1, etc.
-
-To get nodes of specified level, you can apply `having` constraint:
-
-```php
-$result = Category::withDepth()->having('depth', '=', 1)->get();
-```
-
-__IMPORTANT!__ This will not work in database strict mode
-
-#### Default order
-
-All nodes are strictly organized internally. By default, no order is
-applied, so nodes may appear in random order and this doesn't affect
-displaying a tree. You can order nodes by alphabet or other index.
-
-But in some cases hierarchical order is essential. It is required for
-retrieving ancestors and can be used to order menu items.
-
-To apply tree order `defaultOrder` method is used:
-
-```php
-$result = Category::defaultOrder()->get();
-```
-
-You can get nodes in reversed order:
-
-```php
-$result = Category::reversed()->get();
-```
-
-To shift node up or down inside parent to affect default order:
-
-```php
-$bool = $node->down();
-$bool = $node->up();
-
-// Shift node by 3 siblings
-$bool = $node->down(3);
-```
-
-The result of the operation is boolean value of whether the node has changed its
-position.
-
-#### Constraints
-
-Various constraints that can be applied to the query builder:
-
-- __whereIsRoot()__ to get only root nodes;
-- __hasParent()__ to get non-root nodes;
-- __whereIsLeaf()__ to get only leaves;
-- __hasChildren()__ to get non-leave nodes;
-- __whereIsAfter($id)__ to get every node (not just siblings) that are after a node
- with specified id;
-- __whereIsBefore($id)__ to get every node that is before a node with specified id.
-
-Descendants constraints:
-
-```php
-$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();
-
-// Include target node into result set
-$result = Category::whereDescendantOrSelf($node)->get();
-```
-
-Ancestor constraints:
-
-```php
-$result = Category::whereAncestorOf($node)->get();
-$result = Category::whereAncestorOrSelf($id)->get();
-```
-
-`$node` can be either a primary key of the model or model instance.
-
-#### Building a tree
-
-After getting a set of nodes, you can convert it to tree. For example:
-
-```php
-$tree = Category::get()->toTree();
-```
-
-This will fill `parent` and `children` relationships on every node in the set and
-you can render a tree using recursive algorithm:
-
-```php
-$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);
-```
-
-This will output something like this:
-
-```
-- Root
--- Child 1
---- Sub child 1
--- Child 2
-- Another root
-```
-
-##### Building flat tree
-
-Also, you can build a flat tree: a list of nodes where child nodes are immediately
-after parent node. This is helpful when you get nodes with custom order
-(i.e. alphabetically) and don't want to use recursion to iterate over your nodes.
-
-```php
-$nodes = Category::get()->toFlatTree();
-```
-
-Previous example will output:
-
-```
-Root
-Child 1
-Sub child 1
-Child 2
-Another root
-```
-
-##### Getting a subtree
-
-Sometimes you don't need whole tree to be loaded and just some subtree of specific node.
-It is show in following example:
-
-```php
-$root = Category::descendantsAndSelf($rootId)->toTree()->first();
-```
-
-In a single query we are getting a root of a subtree and all of its
-descendants that are accessible via `children` relation.
-
-If you don't need `$root` node itself, do following instead:
-
-```php
-$tree = Category::descendantsOf($rootId)->toTree($rootId);
-```
-
-### Deleting nodes
-
-To delete a node:
-
-```php
-$node->delete();
-```
-
-**IMPORTANT!** Any descendant that node has will also be deleted!
-
-**IMPORTANT!** Nodes are required to be deleted as models, **don't** try do delete them using a query like so:
-
-```php
-Category::where('id', '=', $id)->delete();
-```
-
-This will break the tree!
-
-`SoftDeletes` trait is supported, also on model level.
-
-### Helper methods
-
-To check if node is a descendant of other node:
-
-```php
-$bool = $node->isDescendantOf($parent);
-```
-
-To check whether the node is a root:
-
-```php
-$bool = $node->isRoot();
-```
-
-Other checks:
-
-* `$node->isChildOf($other);`
-* `$node->isAncestorOf($other);`
-* `$node->isSiblingOf($other);`
-* `$node->isLeaf()`
-
-### Checking consistency
-
-You can check whether a tree is broken (i.e. has some structural errors):
-
-```php
-$bool = Category::isBroken();
-```
-
-It is possible to get error statistics:
-
-```php
-$data = Category::countErrors();
-```
-
-It will return an array with following keys:
-
-- `oddness` -- the number of nodes that have wrong set of `lft` and `rgt` values
-- `duplicates` -- the number of nodes that have same `lft` or `rgt` values
-- `wrong_parent` -- the number of nodes that have invalid `parent_id` value that
- doesn't correspond to `lft` and `rgt` values
-- `missing_parent` -- the number of nodes that have `parent_id` pointing to
- node that doesn't exists
-
-#### Fixing tree
-
-Since v3.1 tree can now be fixed. Using inheritance info from `parent_id` column,
-proper `_lft` and `_rgt` values are set for every node.
-
-```php
-Node::fixTree();
-```
-
-### Scoping
-
-Imagine you have `Menu` model and `MenuItems`. There is a one-to-many relationship
-set up between these models. `MenuItem` has `menu_id` attribute for joining models
-together. `MenuItem` incorporates nested sets. It is obvious that you would want to
-process each tree separately based on `menu_id` attribute. In order to do so, you
-need to specify this attribute as scope attribute:
-
-```php
-protected function getScopeAttributes()
-{
- return [ 'menu_id' ];
-}
-```
-
-But now, in order to execute some custom query, you need to provide attributes
-that are used for scoping:
-
-```php
-MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OK
-MenuItem::descendantsOf($id)->get(); // WRONG: returns nodes from other scope
-MenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); // OK
-```
-
-When requesting nodes using model instance, scopes applied automatically based
-on the attributes of that model:
-
-```php
-$node = MenuItem::findOrFail($id);
-
-$node->siblings()->withDepth()->get(); // OK
-```
-
-To get scoped query builder using instance:
-
-```php
-$node->newScopedQuery();
-```
-
-#### Scoping and eager loading
-
-Always use scoped query when eager loading:
-
-```php
-MenuItem::scoped([ 'menu_id' => 5])->with('descendants')->findOrFail($id); // OK
-MenuItem::with('descendants')->findOrFail($id); // WRONG
-```
-
-Requirements
-------------
-
-- PHP >= 5.4
-- Laravel >= 4.1
-
-It is highly suggested to use database that supports transactions (like MySql's InnoDb)
-to secure a tree from possible corruption.
-
-Installation
-------------
-
-To install the package, in terminal:
-
-```
-composer require kalnoy/nestedset
-```
-
-### Setting up from scratch
-
-#### The schema
-
-For Laravel 5.5 and above users:
-
-```php
-Schema::create('table', function (Blueprint $table) {
- ...
- $table->nestedSet();
-});
-
-// To drop columns
-Schema::table('table', function (Blueprint $table) {
- $table->dropNestedSet();
-});
-```
-
-For prior Laravel versions:
-
-```php
-...
-use Kalnoy\Nestedset\NestedSet;
-
-Schema::create('table', function (Blueprint $table) {
- ...
- NestedSet::columns($table);
-});
-```
-
-To drop columns:
-
-```php
-...
-use Kalnoy\Nestedset\NestedSet;
-
-Schema::table('table', function (Blueprint $table) {
- NestedSet::dropColumns($table);
-});
-```
-
-#### The model
-
-Your model should use `Kalnoy\Nestedset\NodeTrait` trait to enable nested sets:
-
-```php
-use Kalnoy\Nestedset\NodeTrait;
-
-class Foo extends Model {
- use NodeTrait;
-}
-```
-
-### Migrating existing data
-
-#### Migrating from other nested set extension
-
-If your previous extension used different set of columns, you just need to override
-following methods on your model class:
-
-```php
-public function getLftName()
-{
- return 'left';
-}
-
-public function getRgtName()
-{
- return 'right';
-}
-
-public function getParentIdName()
-{
- return 'parent';
-}
-
-// Specify parent id attribute mutator
-public function setParentAttribute($value)
-{
- $this->setParentIdAttribute($value);
-}
-```
-
-#### Migrating from basic parentage info
-
-If your tree contains `parent_id` info, you need to add two columns to your schema:
-
-```php
-$table->unsignedInteger('_lft');
-$table->unsignedInteger('_rgt');
-```
-
-After [setting up your model](#the-model) you only need to fix the tree to fill
-`_lft` and `_rgt` columns:
-
-```php
-MyModel::fixTree();
-```
-
-License
-=======
-
-Copyright (c) 2017 Alexander Kalnoy
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..afb71d9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,667 @@
+# Nested Set for Laravel
+
+A Laravel package for storing and querying trees (hierarchical data) in relational
+databases using the [Nested Set Model](http://en.wikipedia.org/wiki/Nested_set_model).
+
+This is a **Lunar-maintained fork** of [`kalnoy/nestedset`](https://github.com/lazychaser/laravel-nestedset)
+by Alexander Kalnoy, kept current for modern Laravel.
+
+> **Supports Laravel 12 & 13 on PHP 8.3+.** If you need an older Laravel version,
+> use the upstream [`kalnoy/nestedset`](https://packagist.org/packages/kalnoy/nestedset).
+
+## Contents
+
+- [What are nested sets?](#what-are-nested-sets)
+- [Requirements](#requirements)
+- [Installation](#installation)
+- [Documentation](#documentation)
+ - [Inserting nodes](#inserting-nodes)
+ - [Retrieving nodes](#retrieving-nodes)
+ - [Deleting nodes](#deleting-nodes)
+ - [Helper methods](#helper-methods)
+ - [Checking consistency](#checking-consistency)
+ - [Scoping](#scoping)
+- [License](#license)
+
+## What are nested sets?
+
+Nested sets, or the [Nested Set Model](http://en.wikipedia.org/wiki/Nested_set_model),
+are a way to effectively store hierarchical data in a relational table. From Wikipedia:
+
+> The nested set model is to number the nodes according to a tree traversal, which
+> visits each node twice, assigning numbers in the order of visiting, and at both
+> visits. This leaves two numbers for each node, which are stored as two attributes.
+> Querying becomes inexpensive: hierarchy membership can be tested by comparing these
+> numbers. Updating requires renumbering and is therefore expensive.
+
+The model performs well when the tree is updated rarely. It is tuned to be fast at
+fetching related nodes, which makes it ideal for things like multi-depth menus or
+shop categories.
+
+## Requirements
+
+- PHP 8.3+
+- Laravel 12 or 13
+
+It is strongly recommended to use a database that supports transactions (such as
+MySQL's InnoDB or PostgreSQL) to protect the tree from corruption during updates.
+
+## Installation
+
+```
+composer require lunarphp/nestedset
+```
+
+### The schema
+
+Add the nested set columns to your table with the `nestedSet()` Blueprint macro:
+
+```php
+Schema::create('table', function (Blueprint $table) {
+ // ...
+ $table->nestedSet();
+});
+
+// To drop the columns:
+Schema::table('table', function (Blueprint $table) {
+ $table->dropNestedSet();
+});
+```
+
+### The model
+
+Add the `Lunar\Nestedset\NodeTrait` trait to your model to enable nested sets:
+
+```php
+use Illuminate\Database\Eloquent\Model;
+use Lunar\Nestedset\NodeTrait;
+
+class Category extends Model
+{
+ use NodeTrait;
+}
+```
+
+## Documentation
+
+Suppose we have a model `Category`. A `$node` variable is an instance of that model
+and the node we are manipulating. It can be a fresh model or one loaded from the
+database.
+
+### Relationships
+
+A node has the following relationships, which are fully functional and can be
+eager-loaded:
+
+- Node belongs to `parent`
+- Node has many `children`
+- Node has many `ancestors`
+- Node has many `descendants`
+
+### Inserting nodes
+
+Moving and inserting nodes runs several database queries, so it is highly
+recommended to wrap these operations in a transaction. Transactions are **not**
+started automatically.
+
+Structural manipulations are **deferred** until you call `save()` on the model.
+Some methods call `save()` implicitly and return the boolean result of the
+operation.
+
+A successful `save()` does not necessarily mean the node was moved. If your
+application depends on whether the node actually changed position, use `hasMoved()`:
+
+```php
+if ($node->save()) {
+ $moved = $node->hasMoved();
+}
+```
+
+#### Creating nodes
+
+When you create a node without specifying a parent, it is appended to the end of
+the tree as a _root_ (a node without a parent):
+
+```php
+Category::create($attributes); // Saved as root
+```
+
+```php
+$node = new Category($attributes);
+$node->save(); // Saved as root
+```
+
+#### Making a node a root
+
+```php
+// Implicit save
+$node->saveAsRoot();
+
+// Explicit save
+$node->makeRoot()->save();
+```
+
+The node is appended to the end of the tree.
+
+#### Appending and prepending to a parent
+
+To make a node a child of another node, you can make it the last or first child.
+*In the following examples, `$parent` is an existing node.*
+
+There are several ways to append a node:
+
+```php
+// #1 Using deferred insert
+$node->appendToNode($parent)->save();
+
+// #2 Using the parent node
+$parent->appendNode($node);
+
+// #3 Using the parent's children relationship
+$parent->children()->create($attributes);
+
+// #4 Using the node's parent relationship
+$node->parent()->associate($parent)->save();
+
+// #5 Using the parent attribute
+$node->parent_id = $parent->id;
+$node->save();
+
+// #6 Using the static method
+Category::create($attributes, $parent);
+```
+
+And a couple of ways to prepend:
+
+```php
+// #1
+$node->prependToNode($parent)->save();
+
+// #2
+$parent->prependNode($node);
+```
+
+#### Inserting before or after a node
+
+You can make `$node` a neighbor of the `$neighbor` node. `$neighbor` must exist;
+the target node may be fresh. If the target node exists, it is moved to the new
+position and its parent is changed if required.
+
+```php
+// Explicit save
+$node->afterNode($neighbor)->save();
+$node->beforeNode($neighbor)->save();
+
+// Implicit save
+$node->insertAfterNode($neighbor);
+$node->insertBeforeNode($neighbor);
+```
+
+#### Building a tree from an array
+
+When you call the static `create` method, it checks whether the attributes contain
+a `children` key. If they do, it creates the child nodes recursively:
+
+```php
+$node = Category::create([
+ 'name' => 'Foo',
+ 'children' => [
+ [
+ 'name' => 'Bar',
+ 'children' => [
+ ['name' => 'Baz'],
+ ],
+ ],
+ ],
+]);
+```
+
+`$node->children` now contains the list of created child nodes.
+
+#### Rebuilding a tree from an array
+
+You can rebuild a tree, which is useful for mass-changing its structure:
+
+```php
+Category::rebuildTree($data, $delete);
+```
+
+`$data` is an array of nodes:
+
+```php
+$data = [
+ ['id' => 1, 'name' => 'foo', 'children' => [ /* ... */ ]],
+ ['name' => 'bar'],
+];
+```
+
+The node named `foo` has an `id`, so the existing node is filled and saved. If the
+node does not exist, a `ModelNotFoundException` is thrown. Its `children` are
+processed in the same way and saved as children of `foo`.
+
+The node named `bar` has no primary key, so it is created.
+
+`$delete` controls whether existing nodes that are not present in `$data` are
+deleted. By default, nodes are not deleted.
+
+##### Rebuilding a subtree
+
+You can constrain rebuilding to the descendants of a given node:
+
+```php
+Category::rebuildSubtree($root, $data);
+```
+
+### Retrieving nodes
+
+*In some examples below, `$id` is the primary key of the target node.*
+
+#### Ancestors and descendants
+
+Ancestors form the chain of parents up to a node — useful for breadcrumbs.
+Descendants are all nodes in a subtree: children, children of children, and so on.
+
+Both can be eager-loaded:
+
+```php
+$node->ancestors;
+$node->descendants;
+```
+
+You can also load them with a custom query:
+
+```php
+$result = Category::ancestorsOf($id);
+$result = Category::ancestorsAndSelf($id);
+$result = Category::descendantsOf($id);
+$result = Category::descendantsAndSelf($id);
+```
+
+In most cases you want ancestors ordered by level:
+
+```php
+$result = Category::defaultOrder()->ancestorsOf($id);
+```
+
+Ancestors can be eager-loaded for breadcrumbs:
+
+```php
+$categories = Category::with('ancestors')->paginate(30);
+```
+
+```blade
+@foreach ($categories as $category)
+ {{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' }}
+ {{ $category->name }}
+@endforeach
+```
+
+#### Siblings
+
+Siblings are nodes that share the same parent:
+
+```php
+$result = $node->getSiblings();
+$result = $node->siblings()->get();
+```
+
+Next siblings:
+
+```php
+// The sibling immediately after the node
+$result = $node->getNextSibling();
+
+// All siblings after the node
+$result = $node->getNextSiblings();
+
+// All siblings after the node, as a query
+$result = $node->nextSiblings()->get();
+```
+
+Previous siblings:
+
+```php
+// The sibling immediately before the node
+$result = $node->getPrevSibling();
+
+// All siblings before the node
+$result = $node->getPrevSiblings();
+
+// All siblings before the node, as a query
+$result = $node->prevSiblings()->get();
+```
+
+#### Getting related models from another table
+
+Imagine each category `has many` goods (a `HasMany` relationship). To get all goods
+of `$category` and every one of its descendants:
+
+```php
+// Get the ids of the descendants
+$categories = $category->descendants()->pluck('id');
+
+// Include the category's own id
+$categories[] = $category->getKey();
+
+// Get the goods
+$goods = Goods::whereIn('category_id', $categories)->get();
+```
+
+#### Including node depth
+
+To know the level of a node:
+
+```php
+$result = Category::withDepth()->find($id);
+
+$depth = $result->depth;
+```
+
+Root nodes are at level 0, their children at level 1, and so on.
+
+To get nodes at a specific level, apply a `having` constraint:
+
+```php
+$result = Category::withDepth()->having('depth', '=', 1)->get();
+```
+
+> **Note:** this will not work in database strict mode.
+
+#### Default order
+
+Nodes are organized internally, but no order is applied by default, so they may
+appear in an arbitrary order. This does not affect displaying a tree, and you are
+free to order nodes alphabetically or by another index.
+
+When hierarchical order is essential (it is required for retrieving ancestors and
+useful for ordering menu items), apply `defaultOrder`:
+
+```php
+$result = Category::defaultOrder()->get();
+```
+
+Reversed order:
+
+```php
+$result = Category::reversed()->get();
+```
+
+To shift a node up or down among its siblings, affecting the default order:
+
+```php
+$bool = $node->down();
+$bool = $node->up();
+
+// Shift the node down by 3 siblings
+$bool = $node->down(3);
+```
+
+The return value is a boolean indicating whether the node changed position.
+
+#### Constraints
+
+Various constraints can be applied to the query builder:
+
+- `whereIsRoot()` — only root nodes
+- `hasParent()` — only non-root nodes
+- `whereIsLeaf()` — only leaves
+- `hasChildren()` — only non-leaf nodes
+- `whereIsAfter($id)` — every node (not just siblings) after the node with the given id
+- `whereIsBefore($id)` — every node before the node with the given id
+
+Descendant constraints:
+
+```php
+$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();
+
+// Include the target node in the result set
+$result = Category::whereDescendantOrSelf($node)->get();
+```
+
+Ancestor constraints:
+
+```php
+$result = Category::whereAncestorOf($node)->get();
+$result = Category::whereAncestorOrSelf($id)->get();
+```
+
+`$node` can be either a primary key or a model instance.
+
+#### Building a tree
+
+After fetching a set of nodes, you can convert it into a tree:
+
+```php
+$tree = Category::get()->toTree();
+```
+
+This fills the `parent` and `children` relationships on every node so you can render
+the tree recursively:
+
+```php
+$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);
+```
+
+This outputs something like:
+
+```
+- Root
+-- Child 1
+--- Sub child 1
+-- Child 2
+- Another root
+```
+
+##### Building a flat tree
+
+You can also build a flat tree: a list of nodes where each child node immediately
+follows its parent. This is helpful when nodes have a custom order (e.g.
+alphabetical) and you want to avoid recursion:
+
+```php
+$nodes = Category::get()->toFlatTree();
+```
+
+The previous example then outputs:
+
+```
+Root
+Child 1
+Sub child 1
+Child 2
+Another root
+```
+
+##### Getting a subtree
+
+Sometimes you only need a subtree rather than the whole tree:
+
+```php
+$root = Category::descendantsAndSelf($rootId)->toTree()->first();
+```
+
+In a single query this gets a subtree root and all of its descendants, accessible
+via the `children` relation.
+
+If you don't need the `$root` node itself:
+
+```php
+$tree = Category::descendantsOf($rootId)->toTree($rootId);
+```
+
+### Deleting nodes
+
+To delete a node:
+
+```php
+$node->delete();
+```
+
+> **Important:** any descendants the node has are also deleted.
+
+> **Important:** nodes must be deleted as models. **Do not** delete them with a
+> query like the following — it will break the tree:
+>
+> ```php
+> Category::where('id', '=', $id)->delete(); // DON'T
+> ```
+
+The `SoftDeletes` trait is supported, including at the model level.
+
+### Helper methods
+
+```php
+$bool = $node->isDescendantOf($parent);
+$bool = $node->isRoot();
+$bool = $node->isChildOf($other);
+$bool = $node->isAncestorOf($other);
+$bool = $node->isSiblingOf($other);
+$bool = $node->isLeaf();
+```
+
+### Checking consistency
+
+Check whether a tree is broken (has structural errors):
+
+```php
+$bool = Category::isBroken();
+```
+
+Get error statistics:
+
+```php
+$data = Category::countErrors();
+```
+
+The returned array has the following keys:
+
+- `oddness` — nodes with a wrong set of `lft`/`rgt` values
+- `duplicates` — nodes that share a `lft` or `rgt` value
+- `wrong_parent` — nodes with a `parent_id` that doesn't match their `lft`/`rgt` values
+- `missing_parent` — nodes with a `parent_id` pointing to a node that doesn't exist
+
+#### Fixing a tree
+
+A broken tree can be fixed. Using the inheritance information from the `parent_id`
+column, the correct `_lft` and `_rgt` values are set for every node:
+
+```php
+Category::fixTree();
+
+// Or constrain it to a subtree:
+Category::fixSubtree($root);
+```
+
+### Scoping
+
+Imagine you have `Menu` and `MenuItem` models with a one-to-many relationship, where
+`MenuItem` has a `menu_id` attribute and uses nested sets. You'd want to process
+each tree separately based on `menu_id`. To do so, declare the scope attribute on
+the model:
+
+```php
+protected function getScopeAttributes()
+{
+ return ['menu_id'];
+}
+```
+
+Now, to run a custom query, you must provide the attributes used for scoping:
+
+```php
+MenuItem::scoped(['menu_id' => 5])->withDepth()->get(); // OK
+MenuItem::descendantsOf($id)->get(); // WRONG: returns nodes from other scopes
+MenuItem::scoped(['menu_id' => 5])->fixTree(); // OK
+```
+
+When requesting nodes from a model instance, scopes are applied automatically based
+on that instance's attributes:
+
+```php
+$node = MenuItem::findOrFail($id);
+
+$node->siblings()->withDepth()->get(); // OK
+```
+
+To get a scoped query builder from an instance:
+
+```php
+$node->newScopedQuery();
+```
+
+#### Scoping and eager loading
+
+Always use a scoped query when eager loading:
+
+```php
+MenuItem::scoped(['menu_id' => 5])->with('descendants')->findOrFail($id); // OK
+MenuItem::with('descendants')->findOrFail($id); // WRONG
+```
+
+## Migrating existing data
+
+### Migrating from another nested set extension
+
+If your previous extension used a different set of columns, override these methods
+on your model:
+
+```php
+public function getLftName()
+{
+ return 'left';
+}
+
+public function getRgtName()
+{
+ return 'right';
+}
+
+public function getParentIdName()
+{
+ return 'parent';
+}
+
+// Map the parent id attribute mutator
+public function setParentAttribute($value)
+{
+ $this->setParentIdAttribute($value);
+}
+```
+
+### Migrating from basic parentage info
+
+If your tree only has `parent_id` information, add the two nested set columns to
+your schema:
+
+```php
+$table->unsignedInteger('_lft');
+$table->unsignedInteger('_rgt');
+```
+
+After [setting up your model](#the-model), fix the tree to populate `_lft` and
+`_rgt`:
+
+```php
+MyModel::fixTree();
+```
+
+## License
+
+Released under the [MIT license](https://opensource.org/licenses/MIT).
+
+This package is a fork of [`kalnoy/nestedset`](https://github.com/lazychaser/laravel-nestedset).
+Copyright (c) 2017 Alexander Kalnoy. Maintained for Lunar by Neon Digital.
diff --git a/TODO.markdown b/TODO.markdown
deleted file mode 100644
index d456273..0000000
--- a/TODO.markdown
+++ /dev/null
@@ -1,2 +0,0 @@
-* Convert query builder to extension
-* Implement tree update algorithm
\ No newline at end of file
diff --git a/UPGRADE.markdown b/UPGRADE.markdown
deleted file mode 100644
index ad337c8..0000000
--- a/UPGRADE.markdown
+++ /dev/null
@@ -1,28 +0,0 @@
-### Upgrading from 4.0 to 4.1
-
-Nested sets feature has been moved to trait `Kalnoy\Nestedset\NodeTrait`, but
-old `Kalnoy\Nestedset\Node` class is still available.
-
-Some methods on trait were renamed (see changelog), but still available on legacy
-node class.
-
-Default order is no longer applied for `siblings()`, `descendants()`,
-`prevNodes`, `nextNodes`.
-
-### Upgrading to 3.0
-
-Some methods were renamed, see changelog for more details.
-
-### Upgrading to 2.0
-
-Calling `$parent->append($node)` and `$parent->prepend($node)` now automatically
-saves `$node`. Those functions returns whether the node was saved.
-
-`ancestorsOf` now return ancestors only, not including target node.
-
-Default order is not applied automatically, so if you need nodes to be in tree-order
-you should call `defaultOrder` on the query.
-
-Since root node is not required now, `NestedSet::createRoot` method has been removed.
-
-`NestedSet::columns` now doesn't create a foreign key for a `parent_id` column.
diff --git a/UPGRADE.md b/UPGRADE.md
new file mode 100644
index 0000000..e44c4b5
--- /dev/null
+++ b/UPGRADE.md
@@ -0,0 +1,15 @@
+### Upgrading to the Lunar fork
+
+This fork renames the root namespace from `Kalnoy\Nestedset` to `Lunar\Nestedset`.
+Update your imports accordingly, for example:
+
+```php
+// Before
+use Kalnoy\Nestedset\NodeTrait;
+
+// After
+use Lunar\Nestedset\NodeTrait;
+```
+
+Replace the dependency `kalnoy/nestedset` with `lunarphp/nestedset` in your
+`composer.json`. This fork requires Laravel 12 or 13 and PHP 8.3+.
diff --git a/composer.json b/composer.json
index 13b1f27..2722237 100644
--- a/composer.json
+++ b/composer.json
@@ -1,8 +1,9 @@
{
- "name": "kalnoy/nestedset",
- "description": "Nested Set Model for Laravel 5.7 and up",
+ "name": "lunarphp/nestedset",
+ "description": "Nested Set Model for Laravel 12+ (Lunar-maintained fork of kalnoy/nestedset)",
"keywords": [
"laravel",
+ "lunar",
"nested sets",
"nsm",
"database",
@@ -13,31 +14,32 @@
{
"name": "Alexander Kalnoy",
"email": "lazychaser@gmail.com"
+ },
+ {
+ "name": "Glenn Jacobs",
+ "email": "glenn@neondigital.co.uk"
}
],
"require": {
- "php": "^8.0",
- "illuminate/support": ">=13.0",
- "illuminate/database": ">=13.0",
- "illuminate/events": ">=13.0"
+ "php": "^8.3",
+ "illuminate/support": "^12.0|^13.0",
+ "illuminate/database": "^12.0|^13.0",
+ "illuminate/events": "^12.0|^13.0"
},
"autoload": {
"psr-4": {
- "Kalnoy\\Nestedset\\": "src/"
+ "Lunar\\Nestedset\\": "src/"
}
},
"require-dev": {
- "phpunit/phpunit": ">=7.0"
+ "phpunit/phpunit": "^11.0"
},
- "minimum-stability": "dev",
+ "minimum-stability": "stable",
"prefer-stable": true,
"extra": {
- "branch-alias": {
- "dev-master": "v5.0.x-dev"
- },
"laravel": {
"providers": [
- "Kalnoy\\Nestedset\\NestedSetServiceProvider"
+ "Lunar\\Nestedset\\NestedSetServiceProvider"
]
}
},
diff --git a/src/AncestorsRelation.php b/src/AncestorsRelation.php
index b59fba2..319af5d 100644
--- a/src/AncestorsRelation.php
+++ b/src/AncestorsRelation.php
@@ -1,6 +1,6 @@
findCategory('mobile');
$nodes = Category::whereBetween('_lft', array(8, 17))->get();
- $tree1 = \Kalnoy\Nestedset\Collection::make($nodes)->toTree(5);
- $tree2 = \Kalnoy\Nestedset\Collection::make($nodes)->toTree($node);
+ $tree1 = \Lunar\Nestedset\Collection::make($nodes)->toTree(5);
+ $tree2 = \Lunar\Nestedset\Collection::make($nodes)->toTree($node);
$this->assertEquals(4, $tree1->count());
$this->assertEquals(4, $tree2->count());
diff --git a/tests/ScopedNodeTest.php b/tests/ScopedNodeTest.php
index 9622fc9..2da34b9 100644
--- a/tests/ScopedNodeTest.php
+++ b/tests/ScopedNodeTest.php
@@ -1,7 +1,7 @@