From cda7f10fee9322355c62f7105513c7e7423a0357 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Thu, 31 Aug 2023 14:12:21 +0800 Subject: [PATCH 1/6] Add path enumerable trait Implements path enumeration hierarchy for models, operating similar to nested set models but not requiring the rigid left/right values. --- src/Database/Traits/PathEnumerable.php | 314 +++++++++++++++++++ tests/Database/Traits/PathEnumerableTest.php | 171 ++++++++++ 2 files changed, 485 insertions(+) create mode 100644 src/Database/Traits/PathEnumerable.php create mode 100644 tests/Database/Traits/PathEnumerableTest.php diff --git a/src/Database/Traits/PathEnumerable.php b/src/Database/Traits/PathEnumerable.php new file mode 100644 index 000000000..6f792653a --- /dev/null +++ b/src/Database/Traits/PathEnumerable.php @@ -0,0 +1,314 @@ +integer('parent_id')->unsigned()->nullable(); + * $table->string('path')->nullable(); + * ``` + * + * @author Ben Thomson + * @copyright Winter CMS + * @link https://www.waitingforcode.com/mysql/managing-hierarchical-data-in-mysql-path-enumeration/read + * @link https://vadimtropashko.wordpress.com/2008/08/09/one-more-nested-intervals-vs-adjacency-list-comparison/ + */ +trait PathEnumerable +{ + /** + * Stores the new parent ID on update. If set to `false`, no change is pending. + */ + protected int|null|false $newParentId = false; + + public static function bootPathEnumerable(): void + { + static::extend(function (Model $model) { + // Define relationships + + $model->hasMany['children'] = [ + get_class($model), + 'key' => $model->getParentColumnName() + ]; + + $model->belongsTo['parent'] = [ + get_class($model), + 'key' => $model->getParentColumnName() + ]; + + // Add event listeners + $model->bindEvent('model.afterCreate', function () use ($model) { + $model->setEnumerablePath(); + }); + + $model->bindEvent('model.beforeUpdate', function () use ($model) { + $model->storeNewParent(); + }); + + $model->bindEvent('model.afterUpdate', function () use ($model) { + $model->moveToNewParent(); + }); + + $model->bindEvent('model.beforeDelete', function () use ($model) { + $model->deleteDescendants(); + }); + + if (static::hasGlobalScope(SoftDeletingScope::class)) { + $model->bindEvent('model.afterRestore', function () use ($model) { + $model->restoreDescendants(); + $model->setEnumerablePath(); + }); + } + }); + } + + /** + * Gets the direct parent of the current record. + */ + public function getParent(): Collection + { + return $this->parent()->get(); + } + + /** + * Gets all ancestral records of the current record. + */ + public function getParents(): Collection + { + return $this->newQuery()->ancestors()->get(); + } + + /** + * Gets all direct children of the current record. + */ + public function getChildren(): Collection + { + return $this->children()->get(); + } + + /** + * Gets all children (ancestors) of the current record. + * + * This will include children records of the child records, and so on. + */ + public function getAllChildren(): Collection + { + return $this->newQuery()->descendants()->get(); + } + + /** + * Root nodes scope. + * + * Gets all record that form the root nodes of the hierarchy. + */ + public function scopeRoot(Builder $query): void + { + $query->whereNull($this->getParentColumnName()); + } + + /** + * Descendants scope. + * + * Gets all children records, and all children of those records, and so on. + */ + public function scopeDescendants(Builder $query): void + { + if (!$this->exists()) { + return; + } + + $query->where($this->getPathColumnName(), 'LIKE', $this->getPath() . '/%'); + } + + /** + * Ancestors scope. + * + * Gets all records that are direct ancestors (parents) of the current record. + */ + public function scopeAncestors(Builder $query): void + { + if (!$this->exists()) { + return; + } + + $query->whereIn($this->getKeyName(), $this->getAncestorIds()); + } + + /** + * Gets the enumerable path on the current record. + * + * This will take into account any parent changes, allowing you to get the new path before the record is saved. + */ + public function getEnumerablePath(): string + { + if ($this->parent()->exists()) { + return $this->parent->path . '/' . $this->id; + } + + return '/' . $this->id; + } + + /** + * Sets the enumerable path on the current record. + */ + public function setEnumerablePath(): void + { + $this->path = $path = $this->getEnumerablePath(); + + $this->newQuery() + ->where($this->getKeyName(), $this->id) + ->update([$this->getPathColumnName() => $path]); + } + + /** + * Stores the new parent ID in preparation for an update. + */ + public function storeNewParent(): void + { + $isDirty = $this->isDirty($this->getParentColumnName()); + + if (!$isDirty) { + return; + } + + $this->newParentId = $this->getParentId(); + } + + /** + * Moves a record, and all of its children, to a new parent. + * + * This will update the enumerated paths of all records. + */ + public function moveToNewParent(): void + { + if ($this->newParentId === false) { + return; + } + + $oldPath = $this->getPath(); + $newPath = $this->getEnumerablePath(); + + $this->getConnection()->transaction(function () use ($oldPath, $newPath) { + foreach ($this->getAllChildren() as $child) { + $child->{$this->getPathColumnName()} = str_replace( + $oldPath . '/', + $newPath . '/', + $child->{$this->getPathColumnName()} + ); + $child->saveQuietly(); + } + }); + + $this->setEnumerablePath(); + $this->newParentId = false; + } + + /** + * Deletes all descendants. + */ + public function deleteDescendants(): void + { + $this->newQuery()->descendants()->delete(); + } + + /** + * Deletes all descendants. + */ + public function restoreDescendants(): void + { + $this->newQuery()->descendants()->restore(); + } + + /** + * Determines the depth of the current record. + * + * A root node is considered a depth of `0`. A child node of a root node is considered a depth of `1`, and so on. + */ + public function getDepth(): int + { + return substr_count($this->getPath(), '/') - 1; + } + + /** + * Gets the parent column name. + */ + public function getParentColumnName(): string + { + return defined('static::PARENT_ID') ? constant('static::PARENT_ID') : 'parent_id'; + } + + /** + * Gets the parent column name. + */ + public function getPathColumnName(): string + { + return defined('static::PATH_COLUMN') ? constant('static::PATH_COLUMN') : 'path'; + } + + /** + * Gets the ID of the parent record for the current record. + * + * This will be `null` if the record has no parent (root node). + */ + public function getParentId(): ?int + { + return $this->getAttribute($this->getParentColumnName()); + } + + /** + * Gets the ID of all direct ancestors of the current record. + * + * @return int[] + */ + public function getAncestorIds(): array + { + $ids = explode('/', $this->getPath()); + array_pop($ids); + return $ids; + } + + /** + * Gets the current path of the record. + */ + public function getPath(): string + { + return $this->getAttribute($this->getPathColumnName()); + } + + /** + * Return a custom TreeCollection collection + * + * @param Model[] $models + */ + public function newCollection(array $models = []): TreeCollection + { + return new TreeCollection($models); + } +} diff --git a/tests/Database/Traits/PathEnumerableTest.php b/tests/Database/Traits/PathEnumerableTest.php new file mode 100644 index 000000000..cbdb65131 --- /dev/null +++ b/tests/Database/Traits/PathEnumerableTest.php @@ -0,0 +1,171 @@ +createTable(); + } + + public function testPathsEnumeratedOnCreate() + { + $grandparents = new TestModelEnumerablePath([ + 'name' => 'Grandparents', + ]); + $parents = new TestModelEnumerablePath([ + 'name' => 'Parents', + ]); + $daughter = new TestModelEnumerablePath([ + 'name' => 'Daughter', + ]); + $child = new TestModelEnumerablePath([ + 'name' => 'Child', + ]); + + $grandparents->save(); + $this->assertEquals('/1', $grandparents->path); + $this->assertEquals(0, $grandparents->getDepth()); + + $parents->parent = $grandparents; + $parents->save(); + $this->assertEquals('/1/2', $parents->path); + $this->assertEquals(1, $parents->getDepth()); + + $daughter->parent = $parents; + $daughter->save(); + $this->assertEquals('/1/2/3', $daughter->path); + $this->assertEquals(2, $daughter->getDepth()); + + $child->parent = $daughter; + $child->save(); + $this->assertEquals('/1/2/3/4', $child->path); + $this->assertEquals(3, $child->getDepth()); + + // Check hierarchy + $hierarchy = $child->getParents(); + $this->assertEquals($grandparents->name, $hierarchy->get(0)->name); + $this->assertEquals($parents->name, $hierarchy->get(1)->name); + $this->assertEquals($daughter->name, $hierarchy->get(2)->name); + + $root = TestModelEnumerablePath::root()->get(); + $this->assertCount(1, $root); + $this->assertEquals($grandparents->name, $root->first()->name); + } + + public function testMoveChildRecordToNewParent() + { + $grandparents = new TestModelEnumerablePath([ + 'name' => 'Grandparents', + ]); + $parents = new TestModelEnumerablePath([ + 'name' => 'Parents', + ]); + $daughter = new TestModelEnumerablePath([ + 'name' => 'Daughter', + ]); + $child = new TestModelEnumerablePath([ + 'name' => 'Child', + ]); + + $grandparents->save(); + $parents->parent = $grandparents; + $parents->save(); + $daughter->parent = $parents; + $daughter->save(); + $child->parent = $daughter; + $child->save(); + + $this->assertEquals('/1/2/3/4', $child->path); + + // Move child + $child->parent = $parents; + $child->save(); + $this->assertEquals('/1/2/4', $child->path); + } + + public function testMoveChildRecordWithAncestorsToNewParent() + { + $grandparents = new TestModelEnumerablePath([ + 'name' => 'Grandparents', + ]); + $parents = new TestModelEnumerablePath([ + 'name' => 'Parents', + ]); + $daughter = new TestModelEnumerablePath([ + 'name' => 'Daughter', + ]); + $child = new TestModelEnumerablePath([ + 'name' => 'Child', + ]); + + $grandparents->save(); + $parents->parent = $grandparents; + $parents->save(); + $daughter->parent = $parents; + $daughter->save(); + $child->parent = $daughter; + $child->save(); + + // Move child + $daughter->parent = $grandparents; + $daughter->save(); + $this->assertEquals('/1/3', $daughter->path); + + // Get new path for child + $child = $child->reload(); + $this->assertEquals('/1/3/4', $child->path); + } + + public function testDeleteRecordWithChildren() + { + $grandparents = new TestModelEnumerablePath([ + 'name' => 'Grandparents', + ]); + $parents = new TestModelEnumerablePath([ + 'name' => 'Parents', + ]); + $daughter = new TestModelEnumerablePath([ + 'name' => 'Daughter', + ]); + $child = new TestModelEnumerablePath([ + 'name' => 'Child', + ]); + + $grandparents->save(); + $parents->parent = $grandparents; + $parents->save(); + $daughter->parent = $parents; + $daughter->save(); + $child->parent = $daughter; + $child->save(); + + // Delete parents + $parents->delete(); + $this->assertEquals(1, TestModelEnumerablePath::count()); + $this->assertNull(TestModelEnumerablePath::find($parents->id)); + $this->assertNull(TestModelEnumerablePath::find($daughter->id)); + $this->assertNull(TestModelEnumerablePath::find($child->id)); + } + + protected function createTable() + { + $this->getBuilder()->create('path_enumerable', function ($table) { + $table->increments('id'); + $table->integer('parent_id')->unsigned()->nullable(); + $table->string('path')->nullable(); + $table->string('name'); + $table->timestamps(); + }); + } +} + +class TestModelEnumerablePath extends \Winter\Storm\Database\Model +{ + use \Winter\Storm\Database\Traits\PathEnumerable; + + public $table = 'path_enumerable'; + public $fillable = [ + 'name', + ]; +} From 62411f87c1839f4565bca3f4c0c7d0e559a7984e Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Thu, 31 Aug 2023 14:23:21 +0800 Subject: [PATCH 2/6] Add getNested method and test --- src/Database/Traits/PathEnumerable.php | 8 +++++ tests/Database/Traits/PathEnumerableTest.php | 31 ++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Database/Traits/PathEnumerable.php b/src/Database/Traits/PathEnumerable.php index 6f792653a..2fdd45a66 100644 --- a/src/Database/Traits/PathEnumerable.php +++ b/src/Database/Traits/PathEnumerable.php @@ -123,6 +123,14 @@ public function getAllChildren(): Collection return $this->newQuery()->descendants()->get(); } + /** + * Gets a nested collection of all records. + */ + public function getNested(): Collection + { + return $this->newQuery()->get()->toNested(); + } + /** * Root nodes scope. * diff --git a/tests/Database/Traits/PathEnumerableTest.php b/tests/Database/Traits/PathEnumerableTest.php index cbdb65131..19c91fc6e 100644 --- a/tests/Database/Traits/PathEnumerableTest.php +++ b/tests/Database/Traits/PathEnumerableTest.php @@ -148,6 +148,37 @@ public function testDeleteRecordWithChildren() $this->assertNull(TestModelEnumerablePath::find($child->id)); } + public function testGetNestedRecords() + { + $grandparents = new TestModelEnumerablePath([ + 'name' => 'Grandparents', + ]); + $parents = new TestModelEnumerablePath([ + 'name' => 'Parents', + ]); + $daughter = new TestModelEnumerablePath([ + 'name' => 'Daughter', + ]); + $child = new TestModelEnumerablePath([ + 'name' => 'Child', + ]); + + $grandparents->save(); + $parents->parent = $grandparents; + $parents->save(); + $daughter->parent = $parents; + $daughter->save(); + $child->parent = $daughter; + $child->save(); + + $grandparents->reload(); + $nested = $grandparents->getNested(); + $this->assertEquals($grandparents->name, $nested->get(1)->name); + $this->assertEquals($parents->name, $nested->get(1)->children->get(0)->name); + $this->assertEquals($daughter->name, $nested->get(1)->children->get(0)->children->get(0)->name); + $this->assertEquals($child->name, $nested->get(1)->children->get(0)->children->get(0)->children->get(0)->name); + } + protected function createTable() { $this->getBuilder()->create('path_enumerable', function ($table) { From 5975793a4eee86b65e8d01254746018cd9825acc Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Thu, 31 Aug 2023 15:41:30 +0800 Subject: [PATCH 3/6] Fix a couple of hard references to path column --- src/Database/Traits/PathEnumerable.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Traits/PathEnumerable.php b/src/Database/Traits/PathEnumerable.php index 2fdd45a66..d2195e5f9 100644 --- a/src/Database/Traits/PathEnumerable.php +++ b/src/Database/Traits/PathEnumerable.php @@ -177,7 +177,7 @@ public function scopeAncestors(Builder $query): void public function getEnumerablePath(): string { if ($this->parent()->exists()) { - return $this->parent->path . '/' . $this->id; + return $this->parent->{$this->getPathColumnName()} . '/' . $this->id; } return '/' . $this->id; @@ -188,7 +188,7 @@ public function getEnumerablePath(): string */ public function setEnumerablePath(): void { - $this->path = $path = $this->getEnumerablePath(); + $this->{$this->getPathColumnName()} = $path = $this->getEnumerablePath(); $this->newQuery() ->where($this->getKeyName(), $this->id) From 1edb90e27e947638e8a3015952cea3c7e39ed1fa Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Mon, 11 Dec 2023 16:00:33 +0800 Subject: [PATCH 4/6] Add ability to define a custom segment column. This allows the path to be made up of segments from a different field, such as folder, or name. By default, path segments are derived from the primary key (usually an ID). --- src/Database/Traits/PathEnumerable.php | 90 ++++++++++++++++++-- tests/Database/Traits/PathEnumerableTest.php | 61 +++++++++++++ 2 files changed, 142 insertions(+), 9 deletions(-) diff --git a/src/Database/Traits/PathEnumerable.php b/src/Database/Traits/PathEnumerable.php index d2195e5f9..ae55d3645 100644 --- a/src/Database/Traits/PathEnumerable.php +++ b/src/Database/Traits/PathEnumerable.php @@ -7,6 +7,7 @@ use Winter\Storm\Database\Collection; use Winter\Storm\Database\Model; use Winter\Storm\Database\TreeCollection; +use Winter\Storm\Exception\ApplicationException; /** * "Enumerable path" model trait @@ -48,6 +49,24 @@ trait PathEnumerable */ protected int|null|false $newParentId = false; + /** + * Defines the column name that will be used for the path segments. By default, the ID of the record will be used. + * + * protected string $segmentColumn = 'id'; + */ + + /** + * Defines the column used for storing the parent ID. By default, this will be `parent_id`. + * + * const PARENT_ID = 'parent_id'; + */ + + /** + * Defines the column used for storing the path. By default, this will be `path`. + * + * const PATH_COLUMN = 'path'; + */ + public static function bootPathEnumerable(): void { static::extend(function (Model $model) { @@ -149,7 +168,8 @@ public function scopeRoot(Builder $query): void public function scopeDescendants(Builder $query): void { if (!$this->exists()) { - return; + // Nullify the query, as this record does not yet exist within the hierarchy + $query->whereRaw('0 = 1'); } $query->where($this->getPathColumnName(), 'LIKE', $this->getPath() . '/%'); @@ -163,10 +183,18 @@ public function scopeDescendants(Builder $query): void public function scopeAncestors(Builder $query): void { if (!$this->exists()) { - return; + // Nullify the query, as this record does not yet exist within the hierarchy + $query->whereRaw('0 = 1'); } - $query->whereIn($this->getKeyName(), $this->getAncestorIds()); + $ancestorPaths = $this->getAncestorPaths(); + + if (!count($ancestorPaths)) { + // Nullify the query, as this record has no ancestors. + $query->whereRaw('0 = 1'); + } + + $query->whereIn($this->getPathColumnName(), $this->getAncestorPaths()); } /** @@ -177,10 +205,42 @@ public function scopeAncestors(Builder $query): void public function getEnumerablePath(): string { if ($this->parent()->exists()) { - return $this->parent->{$this->getPathColumnName()} . '/' . $this->id; + return $this->parent->{$this->getPathColumnName()} . '/' . $this->getEnumerableSegment(); + } + + return '/' . $this->getEnumerableSegment(); + } + + public function getSegmentColumn(): string + { + if (!property_exists($this, 'segmentColumn')) { + return $this->primaryKey; } - return '/' . $this->id; + return $this->segmentColumn; + } + + /** + * Gets the enumerable segment of this record. + * + * By default, this will return the ID of the record to make up each segment of the path. You can change the column + * that makes up the path segments by defining another column name in the `$segmentColumn` property. + * + * @return string + */ + public function getEnumerableSegment(): string + { + if (!array_key_exists($this->getSegmentColumn(), $this->attributes)) { + throw new ApplicationException( + sprintf( + 'The segment column "%s" does not exist on the model "%s".', + $this->segmentColumn, + get_class($this) + ) + ); + } + + return (string) $this->getAttribute($this->getSegmentColumn()); } /** @@ -273,7 +333,7 @@ public function getParentColumnName(): string } /** - * Gets the parent column name. + * Gets the path column name. */ public function getPathColumnName(): string { @@ -291,15 +351,27 @@ public function getParentId(): ?int } /** - * Gets the ID of all direct ancestors of the current record. + * Gets the paths of all direct ancestors of the current record. * * @return int[] */ - public function getAncestorIds(): array + public function getAncestorPaths(): array { $ids = explode('/', $this->getPath()); + array_shift($ids); array_pop($ids); - return $ids; + + if (!count($ids)) { + return []; + } + + $paths = []; + + for ($i = 1; $i <= count($ids); $i++) { + $paths[] = '/' . implode('/', array_slice($ids, 0, $i)); + } + + return $paths; } /** diff --git a/tests/Database/Traits/PathEnumerableTest.php b/tests/Database/Traits/PathEnumerableTest.php index 19c91fc6e..139e971b0 100644 --- a/tests/Database/Traits/PathEnumerableTest.php +++ b/tests/Database/Traits/PathEnumerableTest.php @@ -179,6 +179,55 @@ public function testGetNestedRecords() $this->assertEquals($child->name, $nested->get(1)->children->get(0)->children->get(0)->children->get(0)->name); } + public function testPathsEnumeratedOnCreateWithDifferentSegmentColumn() + { + $grandparents = new TestModelEnumerablePathNameSegment([ + 'name' => 'Grandparents', + ]); + $parents = new TestModelEnumerablePathNameSegment([ + 'name' => 'Parents', + ]); + $daughter = new TestModelEnumerablePathNameSegment([ + 'name' => 'Daughter', + ]); + $child = new TestModelEnumerablePathNameSegment([ + 'name' => 'Child', + ]); + + $grandparents->save(); + $this->assertEquals('/Grandparents', $grandparents->path); + $this->assertEquals(0, $grandparents->getDepth()); + $this->assertEquals(0, $grandparents->getParents()->count()); + + $parents->parent = $grandparents; + $parents->save(); + $this->assertEquals('/Grandparents/Parents', $parents->path); + $this->assertEquals(1, $parents->getDepth()); + $this->assertEquals(1, $parents->getParents()->count()); + + $daughter->parent = $parents; + $daughter->save(); + $this->assertEquals('/Grandparents/Parents/Daughter', $daughter->path); + $this->assertEquals(2, $daughter->getDepth()); + $this->assertEquals(2, $daughter->getParents()->count()); + + $child->parent = $daughter; + $child->save(); + $this->assertEquals('/Grandparents/Parents/Daughter/Child', $child->path); + $this->assertEquals(3, $child->getDepth()); + $this->assertEquals(3, $child->getParents()->count()); + + // Check hierarchy + $hierarchy = $child->getParents(); + $this->assertEquals($grandparents->name, $hierarchy->get(0)->name); + $this->assertEquals($parents->name, $hierarchy->get(1)->name); + $this->assertEquals($daughter->name, $hierarchy->get(2)->name); + + $root = TestModelEnumerablePathNameSegment::root()->get(); + $this->assertCount(1, $root); + $this->assertEquals($grandparents->name, $root->first()->name); + } + protected function createTable() { $this->getBuilder()->create('path_enumerable', function ($table) { @@ -200,3 +249,15 @@ class TestModelEnumerablePath extends \Winter\Storm\Database\Model 'name', ]; } + +class TestModelEnumerablePathNameSegment extends \Winter\Storm\Database\Model +{ + use \Winter\Storm\Database\Traits\PathEnumerable; + + public $table = 'path_enumerable'; + public $fillable = [ + 'name', + ]; + + protected string $segmentColumn = 'name'; +} From 9e573568a0f6d04eb60cc0422fbcd41689de531b Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Tue, 12 Dec 2023 09:24:42 +0800 Subject: [PATCH 5/6] Prevent forward-slashes in segment column from breaking hierarachy --- src/Database/Traits/PathEnumerable.php | 6 +++--- tests/Database/Traits/PathEnumerableTest.php | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Database/Traits/PathEnumerable.php b/src/Database/Traits/PathEnumerable.php index ae55d3645..5825b6e0c 100644 --- a/src/Database/Traits/PathEnumerable.php +++ b/src/Database/Traits/PathEnumerable.php @@ -240,7 +240,7 @@ public function getEnumerableSegment(): string ); } - return (string) $this->getAttribute($this->getSegmentColumn()); + return preg_replace('/(?getAttribute($this->getSegmentColumn())); } /** @@ -321,7 +321,7 @@ public function restoreDescendants(): void */ public function getDepth(): int { - return substr_count($this->getPath(), '/') - 1; + return count(preg_split('/(?getPath())) - 2; } /** @@ -357,7 +357,7 @@ public function getParentId(): ?int */ public function getAncestorPaths(): array { - $ids = explode('/', $this->getPath()); + $ids = preg_split('/(?getPath()); array_shift($ids); array_pop($ids); diff --git a/tests/Database/Traits/PathEnumerableTest.php b/tests/Database/Traits/PathEnumerableTest.php index 139e971b0..7cbd4d27b 100644 --- a/tests/Database/Traits/PathEnumerableTest.php +++ b/tests/Database/Traits/PathEnumerableTest.php @@ -190,8 +190,8 @@ public function testPathsEnumeratedOnCreateWithDifferentSegmentColumn() $daughter = new TestModelEnumerablePathNameSegment([ 'name' => 'Daughter', ]); - $child = new TestModelEnumerablePathNameSegment([ - 'name' => 'Child', + $grandchild = new TestModelEnumerablePathNameSegment([ + 'name' => 'Grandaughter / Grandson', ]); $grandparents->save(); @@ -211,14 +211,14 @@ public function testPathsEnumeratedOnCreateWithDifferentSegmentColumn() $this->assertEquals(2, $daughter->getDepth()); $this->assertEquals(2, $daughter->getParents()->count()); - $child->parent = $daughter; - $child->save(); - $this->assertEquals('/Grandparents/Parents/Daughter/Child', $child->path); - $this->assertEquals(3, $child->getDepth()); - $this->assertEquals(3, $child->getParents()->count()); + $grandchild->parent = $daughter; + $grandchild->save(); + $this->assertEquals('/Grandparents/Parents/Daughter/Grandaughter \/ Grandson', $grandchild->path); + $this->assertEquals(3, $grandchild->getDepth()); + $this->assertEquals(3, $grandchild->getParents()->count()); // Check hierarchy - $hierarchy = $child->getParents(); + $hierarchy = $grandchild->getParents(); $this->assertEquals($grandparents->name, $hierarchy->get(0)->name); $this->assertEquals($parents->name, $hierarchy->get(1)->name); $this->assertEquals($daughter->name, $hierarchy->get(2)->name); From f27ded72e48293cbd2521832c24a82923998d3a4 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Tue, 12 Dec 2023 09:30:54 +0800 Subject: [PATCH 6/6] Fix comments --- src/Database/Traits/PathEnumerable.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Database/Traits/PathEnumerable.php b/src/Database/Traits/PathEnumerable.php index 5825b6e0c..e80cb06ef 100644 --- a/src/Database/Traits/PathEnumerable.php +++ b/src/Database/Traits/PathEnumerable.php @@ -211,6 +211,14 @@ public function getEnumerablePath(): string return '/' . $this->getEnumerableSegment(); } + /** + * Gets the column that determines each segment of the path. + * + * You can change the column that makes up the path segments by defining another column name in the `$segmentColumn` + * property. + * + * @return string + */ public function getSegmentColumn(): string { if (!property_exists($this, 'segmentColumn')) { @@ -272,7 +280,7 @@ public function storeNewParent(): void /** * Moves a record, and all of its children, to a new parent. * - * This will update the enumerated paths of all records. + * This will update the enumerated paths of all records affected. */ public function moveToNewParent(): void { @@ -307,7 +315,7 @@ public function deleteDescendants(): void } /** - * Deletes all descendants. + * Restores all descendants. */ public function restoreDescendants(): void {