From 07b81891c6afbc502f6b4a2d54ab3c95ac5efb00 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Mon, 1 Jun 2026 14:07:36 +0100 Subject: [PATCH 1/7] Fix NestedSet::isNode() trait detection `(array)$node` casts an object to its properties, not its used traits, so isNode() returned false for genuine nodes. Use class_uses_recursive() with strict comparison instead. Add a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/NestedSet.php | 2 +- tests/NodeTest.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/NestedSet.php b/src/NestedSet.php index 3764103..b88b3bc 100644 --- a/src/NestedSet.php +++ b/src/NestedSet.php @@ -77,7 +77,7 @@ public static function getDefaultColumns() */ public static function isNode($node) { - return is_object($node) && in_array(NodeTrait::class, (array)$node); + return is_object($node) && in_array(NodeTrait::class, class_uses_recursive($node), true); } } \ No newline at end of file diff --git a/tests/NodeTest.php b/tests/NodeTest.php index 7a1877c..ae84100 100644 --- a/tests/NodeTest.php +++ b/tests/NodeTest.php @@ -992,6 +992,13 @@ public function testReplication() $this->assertEquals(1, $category->getParentId()); } + public function testIsNodeDetectsTraitUsers() + { + $this->assertTrue(NestedSet::isNode(new Category)); + $this->assertFalse(NestedSet::isNode(new \stdClass)); + $this->assertFalse(NestedSet::isNode('not an object')); + } + } function all($items) From f8bf8ed776697c557cadb9746e56f16f9e004624 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Mon, 1 Jun 2026 14:08:17 +0100 Subject: [PATCH 2/7] Add Laravel Pint and format codebase Adopt the Laravel preset via pint.json and apply it across src/ and tests/. Add composer format/lint scripts. Co-Authored-By: Claude Opus 4.8 (1M context) --- composer.json | 9 +- phpunit.php | 11 +- pint.json | 3 + src/AncestorsRelation.php | 17 +- src/BaseRelation.php | 42 +--- src/Collection.php | 30 ++- src/DescendantsRelation.php | 22 +- src/NestedSet.php | 12 +- src/NestedSetServiceProvider.php | 2 +- src/NodeTrait.php | 222 +++++++----------- src/QueryBuilder.php | 285 ++++++++++------------- tests/NodeTest.php | 347 +++++++++++++++-------------- tests/ScopedNodeTest.php | 61 ++--- tests/data/categories.php | 26 +-- tests/data/menu_items.php | 18 +- tests/models/Category.php | 13 +- tests/models/DuplicateCategory.php | 11 +- tests/models/MenuItem.php | 9 +- 18 files changed, 504 insertions(+), 636 deletions(-) create mode 100644 pint.json diff --git a/composer.json b/composer.json index 2722237..29bd5f7 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ } }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.0", + "laravel/pint": "^1.29" }, "minimum-stability": "stable", "prefer-stable": true, @@ -46,6 +47,12 @@ "scripts": { "test": [ "@php ./vendor/bin/phpunit" + ], + "format": [ + "@php ./vendor/bin/pint" + ], + "lint": [ + "@php ./vendor/bin/pint --test" ] } } diff --git a/phpunit.php b/phpunit.php index 7f72aff..e1ca400 100644 --- a/phpunit.php +++ b/phpunit.php @@ -1,12 +1,15 @@ addConnection([ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'prfx_' ]); -$capsule->setEventDispatcher(new \Illuminate\Events\Dispatcher); +$capsule = new Manager; +$capsule->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'prfx_']); +$capsule->setEventDispatcher(new Dispatcher); $capsule->bootEloquent(); $capsule->setAsGlobal(); include __DIR__.'/tests/models/Category.php'; -include __DIR__.'/tests/models/MenuItem.php'; \ No newline at end of file +include __DIR__.'/tests/models/MenuItem.php'; diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..93061b6 --- /dev/null +++ b/pint.json @@ -0,0 +1,3 @@ +{ + "preset": "laravel" +} diff --git a/src/AncestorsRelation.php b/src/AncestorsRelation.php index 319af5d..73bc910 100644 --- a/src/AncestorsRelation.php +++ b/src/AncestorsRelation.php @@ -13,16 +13,15 @@ class AncestorsRelation extends BaseRelation */ public function addConstraints() { - if ( ! static::$constraints) return; + if (! static::$constraints) { + return; + } $this->query->whereAncestorOf($this->parent) ->applyNestedSetScope(); } /** - * @param Model $model - * @param $related - * * @return bool */ protected function matches(Model $model, $related) @@ -31,9 +30,8 @@ protected function matches(Model $model, $related) } /** - * @param QueryBuilder $query - * @param Model $model - * + * @param QueryBuilder $query + * @param Model $model * @return void */ protected function addEagerConstraint($query, $model) @@ -42,11 +40,6 @@ protected function addEagerConstraint($query, $model) } /** - * @param $hash - * @param $table - * @param $lft - * @param $rgt - * * @return string */ protected function relationExistenceCondition($hash, $table, $lft, $rgt) diff --git a/src/BaseRelation.php b/src/BaseRelation.php index 8c9215c..0f2347d 100644 --- a/src/BaseRelation.php +++ b/src/BaseRelation.php @@ -30,13 +30,10 @@ abstract class BaseRelation extends Relation /** * AncestorsRelation constructor. - * - * @param QueryBuilder $builder - * @param Model $model */ public function __construct(QueryBuilder $builder, Model $model) { - if ( ! NestedSet::isNode($model)) { + if (! NestedSet::isNode($model)) { throw new InvalidArgumentException('Model must be node.'); } @@ -44,40 +41,28 @@ public function __construct(QueryBuilder $builder, Model $model) } /** - * @param Model $model - * @param $related - * * @return bool */ abstract protected function matches(Model $model, $related); /** - * @param QueryBuilder $query - * @param Model $model - * + * @param QueryBuilder $query + * @param Model $model * @return void */ abstract protected function addEagerConstraint($query, $model); /** - * @param $hash - * @param $table - * @param $lft - * @param $rgt - * * @return string */ abstract protected function relationExistenceCondition($hash, $table, $lft, $rgt); /** - * @param EloquentBuilder $query - * @param EloquentBuilder $parent - * @param array $columns - * + * @param array $columns * @return mixed */ public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilder $parent, - $columns = [ '*' ] + $columns = ['*'] ) { $query = $this->getParent()->replicate()->newScopedQuery()->select($columns); @@ -101,9 +86,7 @@ public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilde /** * Initialize the relation on a set of models. * - * @param array $models - * @param string $relation - * + * @param string $relation * @return array */ public function initRelation(array $models, $relation) @@ -114,7 +97,7 @@ public function initRelation(array $models, $relation) /** * Get a relationship join table hash. * - * @param bool $incrementJoinCount + * @param bool $incrementJoinCount * @return string */ public function getRelationCountHash($incrementJoinCount = true) @@ -135,7 +118,6 @@ public function getResults() /** * Set the constraints for an eager load of the relation. * - * @param array $models * * @return void */ @@ -155,10 +137,7 @@ public function addEagerConstraints(array $models) /** * Match the eagerly loaded results to their parents. * - * @param array $models - * @param EloquentCollection $results - * @param string $relation - * + * @param string $relation * @return array */ public function match(array $models, EloquentCollection $results, $relation) @@ -173,9 +152,6 @@ public function match(array $models, EloquentCollection $results, $relation) } /** - * @param Model $model - * @param EloquentCollection $results - * * @return Collection */ protected function matchForModel(Model $model, EloquentCollection $results) @@ -203,7 +179,7 @@ public function getForeignKeyName() return NestedSet::PARENT_ID; } - /** + /** * Get the Qualify plain foreign key. * * @return mixed diff --git a/src/Collection.php b/src/Collection.php index 6178ddd..0f90017 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -16,17 +16,19 @@ class Collection extends BaseCollection */ public function linkNodes() { - if ($this->isEmpty()) return $this; + if ($this->isEmpty()) { + return $this; + } $groupedNodes = $this->groupBy($this->first()->getParentIdName()); /** @var NodeTrait|Model $node */ foreach ($this->items as $node) { - if ( ! $node->getParentId()) { + if (! $node->getParentId()) { $node->setRelation('parent', null); } - $children = $groupedNodes->get($node->getKey(), [ ]); + $children = $groupedNodes->get($node->getKey(), []); /** @var Model|NodeTrait $child */ foreach ($children as $child) { @@ -46,8 +48,7 @@ public function linkNodes() * * If `$root` is provided, the tree will contain only descendants of that node. * - * @param mixed $root - * + * @param mixed $root * @return Collection */ public function toTree($root = false) @@ -58,7 +59,7 @@ public function toTree($root = false) $this->linkNodes(); - $items = [ ]; + $items = []; $root = $this->getRootNodeId($root); @@ -73,8 +74,7 @@ public function toTree($root = false) } /** - * @param mixed $root - * + * @param mixed $root * @return int */ protected function getRootNodeId($root = false) @@ -106,15 +106,16 @@ protected function getRootNodeId($root = false) * Build a list of nodes that retain the order that they were pulled from * the database. * - * @param bool $root - * + * @param bool $root * @return static */ public function toFlatTree($root = false) { $result = new static; - if ($this->isEmpty()) return $result; + if ($this->isEmpty()) { + return $result; + } $groupedNodes = $this->groupBy($this->first()->getParentIdName()); @@ -124,9 +125,7 @@ public function toFlatTree($root = false) /** * Flatten a tree into a non recursive array. * - * @param Collection $groupedNodes - * @param mixed $parentId - * + * @param mixed $parentId * @return $this */ protected function flattenTree(self $groupedNodes, $parentId) @@ -139,5 +138,4 @@ protected function flattenTree(self $groupedNodes, $parentId) return $this; } - -} \ No newline at end of file +} diff --git a/src/DescendantsRelation.php b/src/DescendantsRelation.php index 8b4de7e..07c5ff8 100644 --- a/src/DescendantsRelation.php +++ b/src/DescendantsRelation.php @@ -2,12 +2,10 @@ namespace Lunar\Nestedset; -use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; class DescendantsRelation extends BaseRelation { - /** * Set the base constraints on the relation query. * @@ -15,15 +13,17 @@ class DescendantsRelation extends BaseRelation */ public function addConstraints() { - if ( ! static::$constraints) return; + if (! static::$constraints) { + return; + } $this->query->whereDescendantOf($this->parent) - ->applyNestedSetScope(); + ->applyNestedSetScope(); } /** - * @param QueryBuilder $query - * @param Model $model + * @param QueryBuilder $query + * @param Model $model */ protected function addEagerConstraint($query, $model) { @@ -31,9 +31,6 @@ protected function addEagerConstraint($query, $model) } /** - * @param Model $model - * @param $related - * * @return mixed */ protected function matches(Model $model, $related) @@ -42,15 +39,10 @@ protected function matches(Model $model, $related) } /** - * @param $hash - * @param $table - * @param $lft - * @param $rgt - * * @return string */ protected function relationExistenceCondition($hash, $table, $lft, $rgt) { return "{$hash}.{$lft} between {$table}.{$lft} + 1 and {$table}.{$rgt}"; } -} \ No newline at end of file +} diff --git a/src/NestedSet.php b/src/NestedSet.php index b88b3bc..cdc9e27 100644 --- a/src/NestedSet.php +++ b/src/NestedSet.php @@ -33,8 +33,6 @@ class NestedSet /** * Add default nested set columns to the table. Also create an index. - * - * @param \Illuminate\Database\Schema\Blueprint $table */ public static function columns(Blueprint $table) { @@ -47,8 +45,6 @@ public static function columns(Blueprint $table) /** * Drop NestedSet columns. - * - * @param \Illuminate\Database\Schema\Blueprint $table */ public static function dropColumns(Blueprint $table) { @@ -65,19 +61,17 @@ public static function dropColumns(Blueprint $table) */ public static function getDefaultColumns() { - return [ static::LFT, static::RGT, static::PARENT_ID ]; + return [static::LFT, static::RGT, static::PARENT_ID]; } /** * Replaces instanceof calls for this trait. * - * @param mixed $node - * + * @param mixed $node * @return bool */ public static function isNode($node) { return is_object($node) && in_array(NodeTrait::class, class_uses_recursive($node), true); } - -} \ No newline at end of file +} diff --git a/src/NestedSetServiceProvider.php b/src/NestedSetServiceProvider.php index 247946c..10cd039 100644 --- a/src/NestedSetServiceProvider.php +++ b/src/NestedSetServiceProvider.php @@ -17,4 +17,4 @@ public function register() NestedSet::dropColumns($this); }); } -} \ No newline at end of file +} diff --git a/src/NodeTrait.php b/src/NodeTrait.php index 1f6bb5a..93b02f6 100644 --- a/src/NodeTrait.php +++ b/src/NodeTrait.php @@ -2,11 +2,13 @@ namespace Lunar\Nestedset; +use Carbon\Carbon; use Exception; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Arr; use LogicException; @@ -27,7 +29,7 @@ trait NodeTrait protected $moved = false; /** - * @var \Carbon\Carbon + * @var Carbon */ public static $deletedAt; @@ -72,8 +74,7 @@ public static function bootNodeTrait() /** * Set an action. * - * @param string $action - * + * @param string $action * @return $this */ protected function setNodeAction($action) @@ -90,18 +91,20 @@ protected function callPendingAction() { $this->moved = false; - if ( ! $this->pending && ! $this->exists) { + if (! $this->pending && ! $this->exists) { $this->makeRoot(); } - if ( ! $this->pending) return; + if (! $this->pending) { + return; + } $method = 'action'.ucfirst(array_shift($this->pending)); $parameters = $this->pending; $this->pending = null; - $this->moved = call_user_func_array([ $this, $method ], $parameters); + $this->moved = call_user_func_array([$this, $method], $parameters); } /** @@ -113,7 +116,7 @@ public static function usesSoftDelete() if (is_null($softDelete)) { $softDelete = in_array( - \Illuminate\Database\Eloquent\SoftDeletes::class, + SoftDeletes::class, class_uses_recursive(static::class) ); } @@ -135,7 +138,7 @@ protected function actionRaw() protected function actionRoot() { // Simplest case that do not affect other nodes. - if ( ! $this->exists) { + if (! $this->exists) { $cut = $this->getLowerBound() + 1; $this->setLft($cut); @@ -154,15 +157,13 @@ protected function actionRoot() */ protected function getLowerBound() { - return (int)$this->newNestedSetQuery()->max($this->getRgtName()); + return (int) $this->newNestedSetQuery()->max($this->getRgtName()); } /** * Append or prepend a node to the parent. * - * @param self $parent - * @param bool $prepend - * + * @param bool $prepend * @return bool */ protected function actionAppendOrPrepend(self $parent, $prepend = false) @@ -171,7 +172,7 @@ protected function actionAppendOrPrepend(self $parent, $prepend = false) $cut = $prepend ? $parent->getLft() + 1 : $parent->getRgt(); - if ( ! $this->insertAt($cut)) { + if (! $this->insertAt($cut)) { return false; } @@ -183,8 +184,7 @@ protected function actionAppendOrPrepend(self $parent, $prepend = false) /** * Apply parent model. * - * @param Model|null $value - * + * @param Model|null $value * @return $this */ protected function setParent($value) @@ -198,9 +198,7 @@ protected function setParent($value) /** * Insert node before or after another node. * - * @param self $node - * @param bool $after - * + * @param bool $after * @return bool */ protected function actionBeforeOrAfter(self $node, $after = false) @@ -215,12 +213,14 @@ protected function actionBeforeOrAfter(self $node, $after = false) */ public function refreshNode() { - if ( ! $this->exists || static::$actionsPerformed === 0) return; + if (! $this->exists || static::$actionsPerformed === 0) { + return; + } $attributes = $this->newNestedSetQuery()->getNodeData($this->getKey()); $this->attributes = array_merge($this->attributes, $attributes); -// $this->original = array_merge($this->original, $attributes); + // $this->original = array_merge($this->original, $attributes); } /** @@ -270,7 +270,7 @@ public function siblings() /** * Get the node siblings and the node itself. * - * @return \Lunar\Nestedset\QueryBuilder + * @return QueryBuilder */ public function siblingsAndSelf() { @@ -281,11 +281,10 @@ public function siblingsAndSelf() /** * Get query for the node siblings and the node itself. * - * @param array $columns * - * @return \Illuminate\Database\Eloquent\Collection + * @return EloquentCollection */ - public function getSiblingsAndSelf(array $columns = [ '*' ]) + public function getSiblingsAndSelf(array $columns = ['*']) { return $this->siblingsAndSelf()->get($columns); } @@ -337,7 +336,7 @@ public function prevNodes() /** * Get query ancestors of the node. * - * @return AncestorsRelation + * @return AncestorsRelation */ public function ancestors() { @@ -373,7 +372,6 @@ public function saveAsRoot() /** * Append and save a node. * - * @param self $node * * @return bool */ @@ -385,7 +383,6 @@ public function appendNode(self $node) /** * Prepend and save a node. * - * @param self $node * * @return bool */ @@ -397,7 +394,6 @@ public function prependNode(self $node) /** * Append a node to the new parent. * - * @param self $parent * * @return $this */ @@ -409,7 +405,6 @@ public function appendToNode(self $parent) /** * Prepend a node to the new parent. * - * @param self $parent * * @return $this */ @@ -419,9 +414,7 @@ public function prependToNode(self $parent) } /** - * @param self $parent - * @param bool $prepend - * + * @param bool $prepend * @return self */ public function appendOrPrependTo(self $parent, $prepend = false) @@ -438,7 +431,6 @@ public function appendOrPrependTo(self $parent, $prepend = false) /** * Insert self after a node. * - * @param self $node * * @return $this */ @@ -450,7 +442,6 @@ public function afterNode(self $node) /** * Insert self before node. * - * @param self $node * * @return $this */ @@ -460,9 +451,7 @@ public function beforeNode(self $node) } /** - * @param self $node - * @param bool $after - * + * @param bool $after * @return self */ public function beforeOrAfterNode(self $node, $after = false) @@ -471,7 +460,7 @@ public function beforeOrAfterNode(self $node, $after = false) ->assertNotDescendant($node) ->assertSameScope($node); - if ( ! $this->isSiblingOf($node)) { + if (! $this->isSiblingOf($node)) { $this->setParent($node->getRelationValue('parent')); } @@ -483,7 +472,6 @@ public function beforeOrAfterNode(self $node, $after = false) /** * Insert self after a node and save. * - * @param self $node * * @return bool */ @@ -495,13 +483,14 @@ public function insertAfterNode(self $node) /** * Insert self before a node and save. * - * @param self $node * * @return bool */ public function insertBeforeNode(self $node) { - if ( ! $this->beforeNode($node)->save()) return false; + if (! $this->beforeNode($node)->save()) { + return false; + } // We'll update the target node since it will be moved $node->refreshNode(); @@ -510,10 +499,6 @@ public function insertBeforeNode(self $node) } /** - * @param $lft - * @param $rgt - * @param $parentId - * * @return $this */ public function rawNode($lft, $rgt, $parentId) @@ -526,8 +511,7 @@ public function rawNode($lft, $rgt, $parentId) /** * Move node up given amount of positions. * - * @param int $amount - * + * @param int $amount * @return bool */ public function up($amount = 1) @@ -537,7 +521,9 @@ public function up($amount = 1) ->skip($amount - 1) ->first(); - if ( ! $sibling) return false; + if (! $sibling) { + return false; + } return $this->insertBeforeNode($sibling); } @@ -545,8 +531,7 @@ public function up($amount = 1) /** * Move node down given amount of positions. * - * @param int $amount - * + * @param int $amount * @return bool */ public function down($amount = 1) @@ -556,7 +541,9 @@ public function down($amount = 1) ->skip($amount - 1) ->first(); - if ( ! $sibling) return false; + if (! $sibling) { + return false; + } return $this->insertAfterNode($sibling); } @@ -564,13 +551,12 @@ public function down($amount = 1) /** * Insert node at specific position. * - * @param int $position - * + * @param int $position * @return bool */ protected function insertAt($position) { - ++static::$actionsPerformed; + static::$actionsPerformed++; $result = $this->exists ? $this->moveNode($position) @@ -584,16 +570,17 @@ protected function insertAt($position) * * @since 2.0 * - * @param int $position - * + * @param int $position * @return int */ protected function moveNode($position) { $updated = $this->newNestedSetQuery() - ->moveNode($this->getKey(), $position) > 0; + ->moveNode($this->getKey(), $position) > 0; - if ($updated) $this->refreshNode(); + if ($updated) { + $this->refreshNode(); + } return $updated; } @@ -603,8 +590,7 @@ protected function moveNode($position) * * @since 2.0 * - * @param int $position - * + * @param int $position * @return bool */ protected function insertNode($position) @@ -647,8 +633,6 @@ protected function deleteDescendants() /** * Restore the descendants. - * - * @param $deletedAt */ protected function restoreDescendants($deletedAt) { @@ -684,8 +668,7 @@ public function newNestedSetQuery($table = null) } /** - * @param string|null $table - * + * @param string|null $table * @return QueryBuilder */ public function newScopedQuery($table = null) @@ -694,24 +677,23 @@ public function newScopedQuery($table = null) } /** - * @param mixed $query - * @param string|null $table - * + * @param mixed $query + * @param string|null $table * @return mixed */ public function applyNestedSetScope($query, $table = null) { - if ( ! $scoped = $this->getScopeAttributes()) { + if (! $scoped = $this->getScopeAttributes()) { return $query; } - if ( ! $table) { + if (! $table) { $table = $this->getTable(); } foreach ($scoped as $attribute) { $query->where($table.'.'.$attribute, '=', - $this->getAttributeValue($attribute)); + $this->getAttributeValue($attribute)); } return $query; @@ -726,8 +708,6 @@ protected function getScopeAttributes() } /** - * @param array $attributes - * * @return QueryBuilder */ public static function scoped(array $attributes) @@ -742,7 +722,7 @@ public static function scoped(array $attributes) /** * {@inheritdoc} */ - public function newCollection(array $models = array()) + public function newCollection(array $models = []) { return new Collection($models); } @@ -751,8 +731,6 @@ public function newCollection(array $models = array()) * {@inheritdoc} * * Use `children` key on `$attributes` to create child nodes. - * - * @param self|null $parent */ public static function create(array $attributes = [], ?self $parent = null) { @@ -769,7 +747,7 @@ public static function create(array $attributes = [], ?self $parent = null) // Now create children $relation = new EloquentCollection; - foreach ((array)$children as $child) { + foreach ((array) $children as $child) { $relation->add($child = static::create($child, $instance)); $child->setRelation('parent', $instance); @@ -787,7 +765,9 @@ public static function create(array $attributes = [], ?self $parent = null) */ public function getNodeHeight() { - if ( ! $this->exists) return 2; + if (! $this->exists) { + return 2; + } return $this->getRgt() - $this->getLft() + 1; } @@ -807,13 +787,15 @@ public function getDescendantCount() * * Behind the scenes node is appended to found parent node. * - * @param int $value + * @param int $value * * @throws Exception If parent node doesn't exists */ public function setParentIdAttribute($value) { - if ($this->getParentId() == $value) return; + if ($this->getParentId() == $value) { + return; + } if ($value) { $this->appendToNode($this->newScopedQuery()->findOrFail($value)); @@ -825,7 +807,7 @@ public function setParentIdAttribute($value) /** * Get whether node is root. * - * @return boolean + * @return bool */ public function isRoot() { @@ -843,7 +825,7 @@ public function isLeaf() /** * Get the lft key name. * - * @return string + * @return string */ public function getLftName() { @@ -853,7 +835,7 @@ public function getLftName() /** * Get the rgt key name. * - * @return string + * @return string */ public function getRgtName() { @@ -863,7 +845,7 @@ public function getRgtName() /** * Get the parent id key name. * - * @return string + * @return string */ public function getParentIdName() { @@ -873,7 +855,7 @@ public function getParentIdName() /** * Get the value of the model's lft key. * - * @return integer + * @return int */ public function getLft() { @@ -883,7 +865,7 @@ public function getLft() /** * Get the value of the model's rgt key. * - * @return integer + * @return int */ public function getRgt() { @@ -893,7 +875,7 @@ public function getRgt() /** * Get the value of the model's parent id key. * - * @return integer + * @return int */ public function getParentId() { @@ -905,11 +887,10 @@ public function getParentId() * * This can be either a next sibling or a next sibling of the parent node. * - * @param array $columns * * @return self */ - public function getNextNode(array $columns = [ '*' ]) + public function getNextNode(array $columns = ['*']) { return $this->nextNodes()->defaultOrder()->first($columns); } @@ -919,81 +900,66 @@ public function getNextNode(array $columns = [ '*' ]) * * This can be either a prev sibling or parent node. * - * @param array $columns * * @return self */ - public function getPrevNode(array $columns = [ '*' ]) + public function getPrevNode(array $columns = ['*']) { return $this->prevNodes()->defaultOrder('desc')->first($columns); } /** - * @param array $columns - * * @return Collection */ - public function getAncestors(array $columns = [ '*' ]) + public function getAncestors(array $columns = ['*']) { return $this->ancestors()->get($columns); } /** - * @param array $columns - * * @return Collection|self[] */ - public function getDescendants(array $columns = [ '*' ]) + public function getDescendants(array $columns = ['*']) { return $this->descendants()->get($columns); } /** - * @param array $columns - * * @return Collection|self[] */ - public function getSiblings(array $columns = [ '*' ]) + public function getSiblings(array $columns = ['*']) { return $this->siblings()->get($columns); } /** - * @param array $columns - * * @return Collection|self[] */ - public function getNextSiblings(array $columns = [ '*' ]) + public function getNextSiblings(array $columns = ['*']) { return $this->nextSiblings()->get($columns); } /** - * @param array $columns - * * @return Collection|self[] */ - public function getPrevSiblings(array $columns = [ '*' ]) + public function getPrevSiblings(array $columns = ['*']) { return $this->prevSiblings()->get($columns); } /** - * @param array $columns - * * @return self */ - public function getNextSibling(array $columns = [ '*' ]) + public function getNextSibling(array $columns = ['*']) { return $this->nextSiblings()->defaultOrder()->first($columns); } /** - * @param array $columns - * * @return self */ - public function getPrevSibling(array $columns = [ '*' ]) + public function getPrevSibling(array $columns = ['*']) { return $this->prevSiblings()->defaultOrder('desc')->first($columns); } @@ -1001,7 +967,6 @@ public function getPrevSibling(array $columns = [ '*' ]) /** * Get whether a node is a descendant of other node. * - * @param self $other * * @return bool */ @@ -1015,7 +980,6 @@ public function isDescendantOf(self $other) /** * Get whether a node is itself or a descendant of other node. * - * @param self $other * * @return bool */ @@ -1028,7 +992,6 @@ public function isSelfOrDescendantOf(self $other) /** * Get whether the node is immediate children of other node. * - * @param self $other * * @return bool */ @@ -1040,7 +1003,6 @@ public function isChildOf(self $other) /** * Get whether the node is a sibling of another node. * - * @param self $other * * @return bool */ @@ -1052,7 +1014,6 @@ public function isSiblingOf(self $other) /** * Get whether the node is an ancestor of other node, including immediate parent. * - * @param self $other * * @return bool */ @@ -1064,7 +1025,6 @@ public function isAncestorOf(self $other) /** * Get whether the node is itself or an ancestor of other node, including immediate parent. * - * @param self $other * * @return bool */ @@ -1111,12 +1071,10 @@ protected function hardDeleting() */ public function getBounds() { - return [ $this->getLft(), $this->getRgt() ]; + return [$this->getLft(), $this->getRgt()]; } /** - * @param $value - * * @return $this */ public function setLft($value) @@ -1127,8 +1085,6 @@ public function setLft($value) } /** - * @param $value - * * @return $this */ public function setRgt($value) @@ -1139,8 +1095,6 @@ public function setRgt($value) } /** - * @param $value - * * @return $this */ public function setParentId($value) @@ -1162,8 +1116,6 @@ protected function dirtyBounds() } /** - * @param self $node - * * @return $this */ protected function assertNotDescendant(self $node) @@ -1176,25 +1128,20 @@ protected function assertNotDescendant(self $node) } /** - * @param self $node - * * @return $this */ protected function assertNodeExists(self $node) { - if ( ! $node->getLft() || ! $node->getRgt()) { + if (! $node->getLft() || ! $node->getRgt()) { throw new LogicException('Node must exists.'); } return $this; } - /** - * @param self $node - */ protected function assertSameScope(self $node) { - if ( ! $scoped = $this->getScopeAttributes()) { + if (! $scoped = $this->getScopeAttributes()) { return; } @@ -1205,12 +1152,9 @@ protected function assertSameScope(self $node) } } - /** - * @param self $node - */ protected function isSameScope(self $node): bool { - if ( ! $scoped = $this->getScopeAttributes()) { + if (! $scoped = $this->getScopeAttributes()) { return true; } @@ -1224,9 +1168,7 @@ protected function isSameScope(self $node): bool } /** - * @param array|null $except - * - * @return \Illuminate\Database\Eloquent\Model + * @return Model */ public function replicate(?array $except = null) { diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 20c2980..93521a0 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -2,15 +2,14 @@ namespace Lunar\Nestedset; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Database\Query\Builder as Query; use Illuminate\Database\Query\Builder as BaseQueryBuilder; +use Illuminate\Database\Query\Builder as Query; +use Illuminate\Database\Query\Expression; use Illuminate\Support\Arr; use LogicException; -use Illuminate\Database\Query\Expression; class QueryBuilder extends Builder { @@ -24,9 +23,8 @@ class QueryBuilder extends Builder * * @since 2.0 * - * @param mixed $id - * @param bool $required - * + * @param mixed $id + * @param bool $required * @return array */ public function getNodeData($id, $required = false) @@ -35,14 +33,14 @@ public function getNodeData($id, $required = false) $query->where($this->model->getKeyName(), '=', $id); - $data = $query->first([ $this->model->getLftName(), - $this->model->getRgtName() ]); + $data = $query->first([$this->model->getLftName(), + $this->model->getRgtName()]); - if ( ! $data && $required) { + if (! $data && $required) { throw new ModelNotFoundException; } - return (array)$data; + return (array) $data; } /** @@ -50,9 +48,8 @@ public function getNodeData($id, $required = false) * * @since 2.0 * - * @param mixed $id - * @param bool $required - * + * @param mixed $id + * @param bool $required * @return array */ public function getPlainNodeData($id, $required = false) @@ -77,16 +74,14 @@ public function whereIsRoot() * * @since 2.0 * - * @param mixed $id - * @param bool $andSelf - * - * @param string $boolean - * + * @param mixed $id + * @param bool $andSelf + * @param string $boolean * @return $this */ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') { - $keyName = $this->model->getTable() . '.' . $this->model->getKeyName(); + $keyName = $this->model->getTable().'.'.$this->model->getKeyName(); $model = null; if (NestedSet::isNode($id)) { @@ -100,7 +95,7 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') $valueQuery = $this->model ->newQuery() ->toBase() - ->select("_.".$this->model->getRgtName()) + ->select('_.'.$this->model->getRgtName()) ->from($this->model->getTable().' as _') ->where($this->model->getKeyName(), '=', $id) ->limit(1); @@ -111,12 +106,12 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') } $this->query->whereNested(function ($inner) use ($model, $value, $andSelf, $id, $keyName) { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $wrappedTable = $this->query->getGrammar()->wrapTable($this->model->getTable()); $inner->whereRaw("{$value} between {$wrappedTable}.{$lft} and {$wrappedTable}.{$rgt}"); - if ( ! $andSelf) { + if (! $andSelf) { $inner->where($keyName, '<>', $id); } if ($model !== null) { @@ -130,9 +125,7 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') } /** - * @param $id - * @param bool $andSelf - * + * @param bool $andSelf * @return $this */ public function orWhereAncestorOf($id, $andSelf = false) @@ -141,8 +134,6 @@ public function orWhereAncestorOf($id, $andSelf = false) } /** - * @param $id - * * @return QueryBuilder */ public function whereAncestorOrSelf($id) @@ -155,23 +146,18 @@ public function whereAncestorOrSelf($id) * * @since 2.0 * - * @param mixed $id - * @param array $columns - * - * @return \Lunar\Nestedset\Collection + * @param mixed $id + * @return Collection */ - public function ancestorsOf($id, array $columns = array( '*' )) + public function ancestorsOf($id, array $columns = ['*']) { return $this->whereAncestorOf($id)->get($columns); } /** - * @param $id - * @param array $columns - * - * @return \Lunar\Nestedset\Collection + * @return Collection */ - public function ancestorsAndSelf($id, array $columns = [ '*' ]) + public function ancestorsAndSelf($id, array $columns = ['*']) { return $this->whereAncestorOf($id, true)->get($columns); } @@ -181,16 +167,15 @@ public function ancestorsAndSelf($id, array $columns = [ '*' ]) * * @since 2.0 * - * @param array $values - * @param string $boolean - * @param bool $not - * @param Query $query - * + * @param array $values + * @param string $boolean + * @param bool $not + * @param Query $query * @return $this */ public function whereNodeBetween($values, $boolean = 'and', $not = false, $query = null) { - ($query ?? $this->query)->whereBetween($this->model->getTable() . '.' . $this->model->getLftName(), $values, $boolean, $not); + ($query ?? $this->query)->whereBetween($this->model->getTable().'.'.$this->model->getLftName(), $values, $boolean, $not); return $this; } @@ -200,8 +185,7 @@ public function whereNodeBetween($values, $boolean = 'and', $not = false, $query * * @since 2.0 * - * @param array $values - * + * @param array $values * @return $this */ public function orWhereNodeBetween($values) @@ -214,15 +198,14 @@ public function orWhereNodeBetween($values) * * @since 2.0 * - * @param mixed $id - * @param string $boolean - * @param bool $not - * @param bool $andSelf - * + * @param mixed $id + * @param string $boolean + * @param bool $not + * @param bool $andSelf * @return $this */ public function whereDescendantOf($id, $boolean = 'and', $not = false, - $andSelf = false + $andSelf = false ) { $this->query->whereNested(function (Query $inner) use ($id, $andSelf, $not) { if (NestedSet::isNode($id)) { @@ -236,8 +219,8 @@ public function whereDescendantOf($id, $boolean = 'and', $not = false, } // Don't include the node - if (!$andSelf) { - ++$data[0]; + if (! $andSelf) { + $data[0]++; } return $this->whereNodeBetween($data, 'and', $not, $inner); @@ -247,8 +230,7 @@ public function whereDescendantOf($id, $boolean = 'and', $not = false, } /** - * @param mixed $id - * + * @param mixed $id * @return QueryBuilder */ public function whereNotDescendantOf($id) @@ -257,8 +239,7 @@ public function whereNotDescendantOf($id) } /** - * @param mixed $id - * + * @param mixed $id * @return QueryBuilder */ public function orWhereDescendantOf($id) @@ -267,8 +248,7 @@ public function orWhereDescendantOf($id) } /** - * @param mixed $id - * + * @param mixed $id * @return QueryBuilder */ public function orWhereNotDescendantOf($id) @@ -277,10 +257,8 @@ public function orWhereNotDescendantOf($id) } /** - * @param $id - * @param string $boolean - * @param bool $not - * + * @param string $boolean + * @param bool $not * @return $this */ public function whereDescendantOrSelf($id, $boolean = 'and', $not = false) @@ -293,39 +271,28 @@ public function whereDescendantOrSelf($id, $boolean = 'and', $not = false) * * @since 2.0 * - * @param mixed $id - * @param array $columns - * @param bool $andSelf - * + * @param mixed $id + * @param bool $andSelf * @return Collection */ - public function descendantsOf($id, array $columns = [ '*' ], $andSelf = false) + public function descendantsOf($id, array $columns = ['*'], $andSelf = false) { try { return $this->whereDescendantOf($id, 'and', false, $andSelf)->get($columns); - } - - catch (ModelNotFoundException $e) { + } catch (ModelNotFoundException $e) { return $this->model->newCollection(); } } /** - * @param $id - * @param array $columns - * * @return Collection */ - public function descendantsAndSelf($id, array $columns = [ '*' ]) + public function descendantsAndSelf($id, array $columns = ['*']) { return $this->descendantsOf($id, $columns, true); } /** - * @param $id - * @param $operator - * @param $boolean - * * @return $this */ protected function whereIsBeforeOrAfter($id, $operator, $boolean) @@ -347,9 +314,9 @@ protected function whereIsBeforeOrAfter($id, $operator, $boolean) $value = '('.$valueQuery->toSql().')'; } - list($lft,) = $this->wrappedColumns(); + [$lft] = $this->wrappedColumns(); - $this->query->whereRaw("{$lft} {$operator} {$value}", [ ], $boolean); + $this->query->whereRaw("{$lft} {$operator} {$value}", [], $boolean); return $this; } @@ -359,9 +326,8 @@ protected function whereIsBeforeOrAfter($id, $operator, $boolean) * * @since 2.0 * - * @param mixed $id - * @param string $boolean - * + * @param mixed $id + * @param string $boolean * @return $this */ public function whereIsAfter($id, $boolean = 'and') @@ -374,9 +340,8 @@ public function whereIsAfter($id, $boolean = 'and') * * @since 2.0 * - * @param mixed $id - * @param string $boolean - * + * @param mixed $id + * @param string $boolean * @return $this */ public function whereIsBefore($id, $boolean = 'and') @@ -389,17 +354,15 @@ public function whereIsBefore($id, $boolean = 'and') */ public function whereIsLeaf() { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); return $this->whereRaw("$lft = $rgt - 1"); } /** - * @param array $columns - * * @return Collection */ - public function leaves(array $columns = [ '*']) + public function leaves(array $columns = ['*']) { return $this->whereIsLeaf()->get($columns); } @@ -407,17 +370,18 @@ public function leaves(array $columns = [ '*']) /** * Include depth level into the result. * - * @param string $as - * + * @param string $as * @return $this */ public function withDepth($as = 'depth') { - if ($this->query->columns === null) $this->query->columns = [ '*' ]; + if ($this->query->columns === null) { + $this->query->columns = ['*']; + } $table = $this->wrappedTable(); - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $alias = '_d'; $wrappedAlias = $this->query->getGrammar()->wrapTable($alias); @@ -512,7 +476,7 @@ public function hasParent() */ public function hasChildren() { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $this->query->whereRaw("{$rgt} > {$lft} + 1"); @@ -522,8 +486,7 @@ public function hasChildren() /** * Order by node position. * - * @param string $dir - * + * @param string $dir * @return $this */ public function defaultOrder($dir = 'asc') @@ -548,15 +511,14 @@ public function reversed() /** * Move a node to the new position. * - * @param mixed $key - * @param int $position - * + * @param mixed $key + * @param int $position * @return int */ public function moveNode($key, $position) { - list($lft, $rgt) = $this->model->newNestedSetQuery() - ->getPlainNodeData($key, true); + [$lft, $rgt] = $this->model->newNestedSetQuery() + ->getPlainNodeData($key, true); if ($lft < $position && $position <= $rgt) { throw new LogicException('Cannot move node into itself.'); @@ -585,7 +547,7 @@ public function moveNode($key, $position) $params = compact('lft', 'rgt', 'from', 'to', 'height', 'distance'); - $boundary = [ $from, $to ]; + $boundary = [$from, $to]; $query = $this->toBase()->where(function (Query $inner) use ($boundary) { $inner->whereBetween($this->model->getLftName(), $boundary); @@ -600,9 +562,8 @@ public function moveNode($key, $position) * * @since 2.0 * - * @param int $cut - * @param int $height - * + * @param int $cut + * @param int $height * @return int */ public function makeGap($cut, $height) @@ -622,8 +583,6 @@ public function makeGap($cut, $height) * * @since 2.0 * - * @param array $params - * * @return array */ protected function patch(array $params) @@ -632,7 +591,7 @@ protected function patch(array $params) $columns = []; - foreach ([ $this->model->getLftName(), $this->model->getRgtName() ] as $col) { + foreach ([$this->model->getLftName(), $this->model->getRgtName()] as $col) { $columns[$col] = $this->columnPatch($grammar->wrap($col), $params); } @@ -644,9 +603,7 @@ protected function patch(array $params) * * @since 2.0 * - * @param string $col - * @param array $params - * + * @param string $col * @return string */ protected function columnPatch($col, array $params) @@ -654,7 +611,9 @@ protected function columnPatch($col, array $params) extract($params); /** @var int $height */ - if ($height > 0) $height = '+'.$height; + if ($height > 0) { + $height = '+'.$height; + } if (isset($cut)) { return new Expression("case when {$col} >= {$cut} then {$col}{$height} else {$col} end"); @@ -665,9 +624,11 @@ protected function columnPatch($col, array $params) /** @var int $rgt */ /** @var int $from */ /** @var int $to */ - if ($distance > 0) $distance = '+'.$distance; + if ($distance > 0) { + $distance = '+'.$distance; + } - return new Expression("case ". + return new Expression('case '. "when {$col} between {$lft} and {$rgt} then {$col}{$distance} ". // Move the node "when {$col} between {$from} and {$to} then {$col}{$height} ". // Move other nodes "else {$col} end" @@ -695,7 +656,7 @@ public function countErrors() $checks['wrong_parent'] = $this->getWrongParentQuery(); // Check for nodes that have missing parent - $checks['missing_parent' ] = $this->getMissingParentQuery(); + $checks['missing_parent'] = $this->getMissingParentQuery(); $query = $this->query->newQuery(); @@ -705,7 +666,7 @@ public function countErrors() $query->selectSub($inner, $key); } - return (array)$query->first(); + return (array) $query->first(); } /** @@ -717,10 +678,10 @@ protected function getOdnessQuery() ->newNestedSetQuery() ->toBase() ->whereNested(function (BaseQueryBuilder $inner) { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $inner->whereRaw("{$lft} >= {$rgt}") - ->orWhereRaw("({$rgt} - {$lft}) % 2 = 0"); + ->orWhereRaw("({$rgt} - {$lft}) % 2 = 0"); }); } @@ -744,12 +705,12 @@ protected function getDuplicatesQuery() ->from($this->query->raw("{$table} as {$waFirst}, {$table} {$waSecond}")) ->whereRaw("{$waFirst}.{$keyName} < {$waSecond}.{$keyName}") ->whereNested(function (BaseQueryBuilder $inner) use ($waFirst, $waSecond) { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $inner->orWhereRaw("{$waFirst}.{$lft}={$waSecond}.{$lft}") - ->orWhereRaw("{$waFirst}.{$rgt}={$waSecond}.{$rgt}") - ->orWhereRaw("{$waFirst}.{$lft}={$waSecond}.{$rgt}") - ->orWhereRaw("{$waFirst}.{$rgt}={$waSecond}.{$lft}"); + ->orWhereRaw("{$waFirst}.{$rgt}={$waSecond}.{$rgt}") + ->orWhereRaw("{$waFirst}.{$lft}={$waSecond}.{$rgt}") + ->orWhereRaw("{$waFirst}.{$rgt}={$waSecond}.{$lft}"); }); return $this->model->applyNestedSetScope($query, $secondAlias); @@ -783,11 +744,11 @@ protected function getWrongParentQuery() ->whereRaw("{$waInterm}.{$keyName} <> {$waParent}.{$keyName}") ->whereRaw("{$waInterm}.{$keyName} <> {$waChild}.{$keyName}") ->whereNested(function (BaseQueryBuilder $inner) use ($waInterm, $waChild, $waParent) { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $inner->whereRaw("{$waChild}.{$lft} not between {$waParent}.{$lft} and {$waParent}.{$rgt}") - ->orWhereRaw("{$waChild}.{$lft} between {$waInterm}.{$lft} and {$waInterm}.{$rgt}") - ->whereRaw("{$waInterm}.{$lft} between {$waParent}.{$lft} and {$waParent}.{$rgt}"); + ->orWhereRaw("{$waChild}.{$lft} between {$waInterm}.{$lft} and {$waInterm}.{$rgt}") + ->whereRaw("{$waInterm}.{$lft} between {$waParent}.{$lft} and {$waParent}.{$rgt}"); }); $this->model->applyNestedSetScope($query, $parentAlias); @@ -824,7 +785,7 @@ protected function getMissingParentQuery() $this->model->applyNestedSetScope($existsCheck, $alias); $inner->whereRaw("{$parentIdName} is not null") - ->addWhereExistsQuery($existsCheck, 'and', true); + ->addWhereExistsQuery($existsCheck, 'and', true); }); } @@ -857,8 +818,7 @@ public function isBroken() * * Nodes with invalid parent are saved as roots. * - * @param null|NodeTrait|Model $root - * + * @param null|NodeTrait|Model $root * @return int The number of changed nodes */ public function fixTree($root = null) @@ -884,8 +844,7 @@ public function fixTree($root = null) } /** - * @param NodeTrait|Model $root - * + * @param NodeTrait|Model $root * @return int */ public function fixSubtree($root) @@ -894,9 +853,7 @@ public function fixSubtree($root) } /** - * @param array $dictionary - * @param NodeTrait|Model|null $parent - * + * @param NodeTrait|Model|null $parent * @return int */ protected function fixNodes(array &$dictionary, $parent = null) @@ -910,7 +867,7 @@ protected function fixNodes(array &$dictionary, $parent = null) $cut = self::reorderNodes($dictionary, $updated, $parentId, $cut); // Save nodes that have invalid parent as roots - while ( ! empty($dictionary)) { + while (! empty($dictionary)) { $dictionary[null] = reset($dictionary); unset($dictionary[key($dictionary)]); @@ -932,18 +889,15 @@ protected function fixNodes(array &$dictionary, $parent = null) } /** - * @param array $dictionary - * @param array $updated - * @param $parentId - * @param int $cut - * + * @param int $cut * @return int + * * @internal param int $fixed */ protected static function reorderNodes( array &$dictionary, array &$updated, $parentId = null, $cut = 1 ) { - if ( ! isset($dictionary[$parentId])) { + if (! isset($dictionary[$parentId])) { return $cut; } @@ -957,7 +911,7 @@ protected static function reorderNodes( $updated[] = $model; } - ++$cut; + $cut++; } unset($dictionary[$parentId]); @@ -970,11 +924,9 @@ protected static function reorderNodes( * * If item data does not contain primary key, new node will be created. * - * @param array $data - * @param bool $delete Whether to delete nodes that exists but not in the data - * array - * @param null $root - * + * @param bool $delete Whether to delete nodes that exists but not in the data + * array + * @param null $root * @return int */ public function rebuildTree(array $data, $delete = false, $root = null) @@ -996,7 +948,7 @@ public function rebuildTree(array $data, $delete = false, $root = null) $this->buildRebuildDictionary($dictionary, $data, $existing, $parentId); /** @var Model|NodeTrait $model */ - if ( ! empty($existing)) { + if (! empty($existing)) { if ($delete && ! $this->model->usesSoftDelete()) { $this->model ->newScopedQuery() @@ -1021,10 +973,7 @@ public function rebuildTree(array $data, $delete = false, $root = null) } /** - * @param $root - * @param array $data - * @param bool $delete - * + * @param bool $delete * @return int */ public function rebuildSubtree($root, array $data, $delete = false) @@ -1033,28 +982,24 @@ public function rebuildSubtree($root, array $data, $delete = false) } /** - * @param array $dictionary - * @param array $data - * @param array $existing - * @param mixed $parentId + * @param mixed $parentId */ protected function buildRebuildDictionary(array &$dictionary, - array $data, - array &$existing, - $parentId = null + array $data, + array &$existing, + $parentId = null ) { $keyName = $this->model->getKeyName(); foreach ($data as $itemData) { /** @var NodeTrait|Model $model */ - - if ( ! isset($itemData[$keyName])) { + if (! isset($itemData[$keyName])) { $model = $this->model->newInstance($this->model->getAttributes()); // Set some values that will be fixed later $model->rawNode(0, 0, $parentId); } else { - if ( ! isset($existing[$key = $itemData[$keyName]])) { + if (! isset($existing[$key = $itemData[$keyName]])) { throw new ModelNotFoundException; } @@ -1070,18 +1015,19 @@ protected function buildRebuildDictionary(array &$dictionary, $dictionary[$parentId][] = $model; - if ( ! isset($itemData['children'])) continue; + if (! isset($itemData['children'])) { + continue; + } $this->buildRebuildDictionary($dictionary, - $itemData['children'], - $existing, - $model->getKey()); + $itemData['children'], + $existing, + $model->getKey()); } } /** - * @param string|null $table - * + * @param string|null $table * @return $this */ public function applyNestedSetScope($table = null) @@ -1092,7 +1038,6 @@ public function applyNestedSetScope($table = null) /** * Get the root node. * - * @param array $columns * * @return self */ diff --git a/tests/NodeTest.php b/tests/NodeTest.php index ae84100..3e84bf1 100644 --- a/tests/NodeTest.php +++ b/tests/NodeTest.php @@ -1,9 +1,13 @@ create('categories', function (\Illuminate\Database\Schema\Blueprint $table) { + $schema->create('categories', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->softDeletes(); @@ -23,7 +27,7 @@ public static function setUpBeforeClass(): void Capsule::enableQueryLog(); } - public function setUp(): void + protected function setUp(): void { $data = include __DIR__.'/data/categories.php'; @@ -36,7 +40,7 @@ public function setUp(): void date_default_timezone_set('America/Denver'); } - public function tearDown(): void + protected function tearDown(): void { Capsule::table('categories')->truncate(); } @@ -51,7 +55,7 @@ public function tearDown(): void public function assertTreeNotBroken($table = 'categories') { - $checks = array(); + $checks = []; $connection = Capsule::connection(); @@ -62,11 +66,11 @@ public function assertTreeNotBroken($table = 'categories') // Check if lft and rgt values are unique $checks[] = "from $table c1, $table c2 where c1.id <> c2.id and ". - "(c1._lft=c2._lft or c1._rgt=c2._rgt or c1._lft=c2._rgt or c1._rgt=c2._lft)"; + '(c1._lft=c2._lft or c1._rgt=c2._rgt or c1._lft=c2._rgt or c1._rgt=c2._lft)'; // Check if parent_id is set correctly $checks[] = "from $table c, $table p, $table m where c.parent_id=p.id and m.id <> p.id and m.id <> c.id and ". - "(c._lft not between p._lft and p._rgt or c._lft between m._lft and m._rgt and m._lft between p._lft and p._rgt)"; + '(c._lft not between p._lft and p._rgt or c._lft between m._lft and m._rgt and m._lft between p._lft and p._rgt)'; foreach ($checks as $i => $check) { $checks[$i] = 'select 1 as error '.$check; @@ -77,17 +81,19 @@ public function assertTreeNotBroken($table = 'categories') $actual = $connection->selectOne($sql); $this->assertEquals(null, $actual->errors, "The tree structure of $table is broken!"); - $actual = (array)Capsule::connection()->selectOne($sql); + $actual = (array) Capsule::connection()->selectOne($sql); - $this->assertEquals(array('errors' => null), $actual, "The tree structure of $table is broken!"); + $this->assertEquals(['errors' => null], $actual, "The tree structure of $table is broken!"); } public function dumpTree($items = null) { - if ( ! $items) $items = Category::withTrashed()->defaultOrder()->get(); + if (! $items) { + $items = Category::withTrashed()->defaultOrder()->get(); + } foreach ($items as $item) { - echo PHP_EOL.($item->trashed() ? '-' : '+').' '.$item->name." ".$item->getKey().' '.$item->getLft()." ".$item->getRgt().' '.$item->getParentId(); + echo PHP_EOL.($item->trashed() ? '-' : '+').' '.$item->name.' '.$item->getKey().' '.$item->getLft().' '.$item->getRgt().' '.$item->getParentId(); } } @@ -98,16 +104,14 @@ public function assertNodeReceivesValidValues($node) $nodeInDb = $this->findCategory($node->name); $this->assertEquals( - [ $nodeInDb->getLft(), $nodeInDb->getRgt() ], - [ $lft, $rgt ], + [$nodeInDb->getLft(), $nodeInDb->getRgt()], + [$lft, $rgt], 'Node is not synced with database after save.' ); } /** - * @param $name - * - * @return \Category + * @return Category */ public function findCategory($name, $withTrashed = false) { @@ -118,7 +122,7 @@ public function findCategory($name, $withTrashed = false) return $q->whereName($name)->first(); } - public function testTreeNotBroken() + public function test_tree_not_broken() { $this->assertTreeNotBroken(); $this->assertFalse(Category::isBroken()); @@ -126,29 +130,29 @@ public function testTreeNotBroken() public function nodeValues($node) { - return array($node->_lft, $node->_rgt, $node->parent_id); + return [$node->_lft, $node->_rgt, $node->parent_id]; } - public function testGetsNodeData() + public function test_gets_node_data() { $data = Category::getNodeData(3); - $this->assertEquals([ '_lft' => 3, '_rgt' => 4 ], $data); + $this->assertEquals(['_lft' => 3, '_rgt' => 4], $data); } - public function testGetsPlainNodeData() + public function test_gets_plain_node_data() { $data = Category::getPlainNodeData(3); - $this->assertEquals([ 3, 4 ], $data); + $this->assertEquals([3, 4], $data); } - public function testReceivesValidValuesWhenAppendedTo() + public function test_receives_valid_values_when_appended_to() { - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $root = Category::root(); - $accepted = array($root->_rgt, $root->_rgt + 1, $root->id); + $accepted = [$root->_rgt, $root->_rgt + 1, $root->id]; $root->appendNode($node); @@ -159,45 +163,45 @@ public function testReceivesValidValuesWhenAppendedTo() $this->assertTrue($node->isDescendantOf($root)); } - public function testReceivesValidValuesWhenPrependedTo() + public function test_receives_valid_values_when_prepended_to() { $root = Category::root(); - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $root->prependNode($node); $this->assertTrue($node->hasMoved()); - $this->assertEquals(array($root->_lft + 1, $root->_lft + 2, $root->id), $this->nodeValues($node)); + $this->assertEquals([$root->_lft + 1, $root->_lft + 2, $root->id], $this->nodeValues($node)); $this->assertTreeNotBroken(); $this->assertTrue($node->isDescendantOf($root)); $this->assertTrue($root->isAncestorOf($node)); $this->assertTrue($node->isChildOf($root)); } - public function testReceivesValidValuesWhenInsertedAfter() + public function test_receives_valid_values_when_inserted_after() { $target = $this->findCategory('apple'); - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $node->afterNode($target)->save(); $this->assertTrue($node->hasMoved()); - $this->assertEquals(array($target->_rgt + 1, $target->_rgt + 2, $target->parent->id), $this->nodeValues($node)); + $this->assertEquals([$target->_rgt + 1, $target->_rgt + 2, $target->parent->id], $this->nodeValues($node)); $this->assertTreeNotBroken(); $this->assertFalse($node->isDirty()); $this->assertTrue($node->isSiblingOf($target)); } - public function testReceivesValidValuesWhenInsertedBefore() + public function test_receives_valid_values_when_inserted_before() { $target = $this->findCategory('apple'); - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $node->beforeNode($target)->save(); $this->assertTrue($node->hasMoved()); - $this->assertEquals(array($target->_lft, $target->_lft + 1, $target->parent->id), $this->nodeValues($node)); + $this->assertEquals([$target->_lft, $target->_lft + 1, $target->parent->id], $this->nodeValues($node)); $this->assertTreeNotBroken(); } - public function testCategoryMovesDown() + public function test_category_moves_down() { $node = $this->findCategory('apple'); $target = $this->findCategory('mobile'); @@ -209,7 +213,7 @@ public function testCategoryMovesDown() $this->assertTreeNotBroken(); } - public function testCategoryMovesUp() + public function test_category_moves_up() { $node = $this->findCategory('samsung'); $target = $this->findCategory('notebooks'); @@ -221,7 +225,7 @@ public function testCategoryMovesUp() $this->assertNodeReceivesValidValues($node); } - public function testFailsToInsertIntoChild() + public function test_fails_to_insert_into_child() { $this->expectException(Exception::class); @@ -231,7 +235,7 @@ public function testFailsToInsertIntoChild() $node->afterNode($target)->save(); } - public function testFailsToAppendIntoItself() + public function test_fails_to_append_into_itself() { $this->expectException(Exception::class); @@ -240,7 +244,7 @@ public function testFailsToAppendIntoItself() $node->appendToNode($node)->save(); } - public function testFailsToPrependIntoItself() + public function test_fails_to_prepend_into_itself() { $this->expectException(Exception::class); @@ -249,40 +253,40 @@ public function testFailsToPrependIntoItself() $node->prependTo($node)->save(); } - public function testWithoutRootWorks() + public function test_without_root_works() { $result = Category::withoutRoot()->pluck('name'); $this->assertNotEquals('store', $result); } - public function testAncestorsReturnsAncestorsWithoutNodeItself() + public function test_ancestors_returns_ancestors_without_node_itself() { $node = $this->findCategory('apple'); $path = all($node->ancestors()->pluck('name')); - $this->assertEquals(array('store', 'notebooks'), $path); + $this->assertEquals(['store', 'notebooks'], $path); } - public function testGetsAncestorsByStatic() + public function test_gets_ancestors_by_static() { $path = all(Category::ancestorsOf(3)->pluck('name')); - $this->assertEquals(array('store', 'notebooks'), $path); + $this->assertEquals(['store', 'notebooks'], $path); } - public function testGetsAncestorsDirect() + public function test_gets_ancestors_direct() { $path = all(Category::find(8)->getAncestors()->pluck('id')); - $this->assertEquals(array(1, 5, 7), $path); + $this->assertEquals([1, 5, 7], $path); } - public function testDescendants() + public function test_descendants() { $node = $this->findCategory('mobile'); $descendants = all($node->descendants()->pluck('name')); - $expected = array('nokia', 'samsung', 'galaxy', 'sony', 'lenovo'); + $expected = ['nokia', 'samsung', 'galaxy', 'sony', 'lenovo']; $this->assertEquals($expected, $descendants); @@ -292,35 +296,35 @@ public function testDescendants() $this->assertEquals($expected, $descendants); $descendants = all(Category::descendantsAndSelf(7)->pluck('name')); - $expected = [ 'samsung', 'galaxy' ]; + $expected = ['samsung', 'galaxy']; $this->assertEquals($expected, $descendants); } - public function testWithDepthWorks() + public function test_with_depth_works() { $nodes = all(Category::withDepth()->limit(4)->pluck('depth')); - $this->assertEquals(array(0, 1, 2, 2), $nodes); + $this->assertEquals([0, 1, 2, 2], $nodes); } - public function testWithDepthWithCustomKeyWorks() + public function test_with_depth_with_custom_key_works() { $node = Category::whereIsRoot()->withDepth('level')->first(); $this->assertTrue(isset($node['level'])); } - public function testWithDepthWorksAlongWithDefaultKeys() + public function test_with_depth_works_along_with_default_keys() { $node = Category::withDepth()->first(); $this->assertTrue(isset($node->name)); } - public function testParentIdAttributeAccessorAppendsNode() + public function test_parent_id_attribute_accessor_appends_node() { - $node = new Category(array('name' => 'lg', 'parent_id' => 5)); + $node = new Category(['name' => 'lg', 'parent_id' => 5]); $node->save(); $this->assertEquals(5, $node->parent_id); @@ -335,7 +339,7 @@ public function testParentIdAttributeAccessorAppendsNode() $this->assertTrue($node->isRoot()); } - public function testFailsToSaveNodeUntilNotInserted() + public function test_fails_to_save_node_until_not_inserted() { $this->expectException(Exception::class); @@ -343,21 +347,21 @@ public function testFailsToSaveNodeUntilNotInserted() $node->save(); } - public function testNodeIsDeletedWithDescendants() + public function test_node_is_deleted_with_descendants() { $node = $this->findCategory('mobile'); $node->forceDelete(); $this->assertTreeNotBroken(); - $nodes = Category::whereIn('id', array(5, 6, 7, 8, 9))->count(); + $nodes = Category::whereIn('id', [5, 6, 7, 8, 9])->count(); $this->assertEquals(0, $nodes); $root = Category::root(); $this->assertEquals(8, $root->getRgt()); } - public function testNodeIsSoftDeleted() + public function test_node_is_soft_deleted() { $root = Category::root(); @@ -373,7 +377,7 @@ public function testNodeIsSoftDeleted() $node = $this->findCategory('mobile'); $node->delete(); - $nodes = Category::whereIn('id', array(5, 6, 7, 8, 9))->count(); + $nodes = Category::whereIn('id', [5, 6, 7, 8, 9])->count(); $this->assertEquals(0, $nodes); $originalRgt = $root->getRgt(); @@ -389,7 +393,7 @@ public function testNodeIsSoftDeleted() $this->assertNotNull($this->findCategory('nokia')); } - public function testSoftDeletedNodeisDeletedWhenParentIsDeleted() + public function test_soft_deleted_nodeis_deleted_when_parent_is_deleted() { $this->findCategory('samsung')->delete(); @@ -401,34 +405,34 @@ public function testSoftDeletedNodeisDeletedWhenParentIsDeleted() $this->assertNull($this->findCategory('sony')); } - public function testFailsToSaveNodeUntilParentIsSaved() + public function test_fails_to_save_node_until_parent_is_saved() { $this->expectException(Exception::class); - $node = new Category(array('title' => 'Node')); - $parent = new Category(array('title' => 'Parent')); + $node = new Category(['title' => 'Node']); + $parent = new Category(['title' => 'Parent']); $node->appendTo($parent)->save(); } - public function testSiblings() + public function test_siblings() { $node = $this->findCategory('samsung'); $siblings = all($node->siblings()->pluck('id')); $next = all($node->nextSiblings()->pluck('id')); $prev = all($node->prevSiblings()->pluck('id')); - $this->assertEquals(array(6, 9, 10), $siblings); - $this->assertEquals(array(9, 10), $next); - $this->assertEquals(array(6), $prev); + $this->assertEquals([6, 9, 10], $siblings); + $this->assertEquals([9, 10], $next); + $this->assertEquals([6], $prev); $siblings = all($node->getSiblings()->pluck('id')); $next = all($node->getNextSiblings()->pluck('id')); $prev = all($node->getPrevSiblings()->pluck('id')); - $this->assertEquals(array(6, 9, 10), $siblings); - $this->assertEquals(array(9, 10), $next); - $this->assertEquals(array(6), $prev); + $this->assertEquals([6, 9, 10], $siblings); + $this->assertEquals([9, 10], $next); + $this->assertEquals([6], $prev); $next = $node->getNextSibling(); $prev = $node->getPrevSibling(); @@ -437,7 +441,7 @@ public function testSiblings() $this->assertEquals(6, $prev->id); } - public function testFetchesReversed() + public function test_fetches_reversed() { $node = $this->findCategory('sony'); $siblings = $node->prevSiblings()->reversed()->value('id'); @@ -445,9 +449,9 @@ public function testFetchesReversed() $this->assertEquals(7, $siblings); } - public function testToTreeBuildsWithDefaultOrder() + public function test_to_tree_builds_with_default_order() { - $tree = Category::whereBetween('_lft', array(8, 17))->defaultOrder()->get()->toTree(); + $tree = Category::whereBetween('_lft', [8, 17])->defaultOrder()->get()->toTree(); $this->assertEquals(1, count($tree)); @@ -456,9 +460,9 @@ public function testToTreeBuildsWithDefaultOrder() $this->assertEquals(4, count($root->children)); } - public function testToTreeBuildsWithCustomOrder() + public function test_to_tree_builds_with_custom_order() { - $tree = Category::whereBetween('_lft', array(8, 17)) + $tree = Category::whereBetween('_lft', [8, 17]) ->orderBy('title') ->get() ->toTree(); @@ -471,28 +475,28 @@ public function testToTreeBuildsWithCustomOrder() $this->assertEquals($root, $root->children->first()->parent); } - public function testToTreeWithSpecifiedRoot() + public function test_to_tree_with_specified_root() { $node = $this->findCategory('mobile'); - $nodes = Category::whereBetween('_lft', array(8, 17))->get(); + $nodes = Category::whereBetween('_lft', [8, 17])->get(); - $tree1 = \Lunar\Nestedset\Collection::make($nodes)->toTree(5); - $tree2 = \Lunar\Nestedset\Collection::make($nodes)->toTree($node); + $tree1 = Collection::make($nodes)->toTree(5); + $tree2 = Collection::make($nodes)->toTree($node); $this->assertEquals(4, $tree1->count()); $this->assertEquals(4, $tree2->count()); } - public function testToTreeBuildsWithDefaultOrderAndMultipleRootNodes() + public function test_to_tree_builds_with_default_order_and_multiple_root_nodes() { $tree = Category::withoutRoot()->get()->toTree(); $this->assertEquals(2, count($tree)); } - public function testToTreeBuildsWithRootItemIdProvided() + public function test_to_tree_builds_with_root_item_id_provided() { - $tree = Category::whereBetween('_lft', array(8, 17))->get()->toTree(5); + $tree = Category::whereBetween('_lft', [8, 17])->get()->toTree(5); $this->assertEquals(4, count($tree)); @@ -501,7 +505,7 @@ public function testToTreeBuildsWithRootItemIdProvided() $this->assertEquals(1, count($root->children)); } - public function testRetrievesNextNode() + public function test_retrieves_next_node() { $node = $this->findCategory('apple'); $next = $node->nextNodes()->first(); @@ -509,7 +513,7 @@ public function testRetrievesNextNode() $this->assertEquals('lenovo', $next->name); } - public function testRetrievesPrevNode() + public function test_retrieves_prev_node() { $node = $this->findCategory('apple'); $next = $node->getPrevNode(); @@ -517,24 +521,24 @@ public function testRetrievesPrevNode() $this->assertEquals('notebooks', $next->name); } - public function testMultipleAppendageWorks() + public function test_multiple_appendage_works() { $parent = $this->findCategory('mobile'); - $child = new Category([ 'name' => 'test' ]); + $child = new Category(['name' => 'test']); $parent->appendNode($child); - $child->appendNode(new Category([ 'name' => 'sub' ])); + $child->appendNode(new Category(['name' => 'sub'])); - $parent->appendNode(new Category([ 'name' => 'test2' ])); + $parent->appendNode(new Category(['name' => 'test2'])); $this->assertTreeNotBroken(); } - public function testDefaultCategoryIsSavedAsRoot() + public function test_default_category_is_saved_as_root() { - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $node->save(); $this->assertEquals(23, $node->_lft); @@ -543,7 +547,7 @@ public function testDefaultCategoryIsSavedAsRoot() $this->assertTrue($node->isRoot()); } - public function testExistingCategorySavedAsRoot() + public function test_existing_category_saved_as_root() { $node = $this->findCategory('apple'); $node->saveAsRoot(); @@ -552,7 +556,7 @@ public function testExistingCategorySavedAsRoot() $this->assertTrue($node->isRoot()); } - public function testNodeMovesDownSeveralPositions() + public function test_node_moves_down_several_positions() { $node = $this->findCategory('nokia'); @@ -561,7 +565,7 @@ public function testNodeMovesDownSeveralPositions() $this->assertEquals($node->_lft, 15); } - public function testNodeMovesUpSeveralPositions() + public function test_node_moves_up_several_positions() { $node = $this->findCategory('sony'); @@ -570,19 +574,19 @@ public function testNodeMovesUpSeveralPositions() $this->assertEquals($node->_lft, 9); } - public function testCountsTreeErrors() + public function test_counts_tree_errors() { $errors = Category::countErrors(); - $this->assertEquals([ 'oddness' => 0, - 'duplicates' => 0, - 'wrong_parent' => 0, - 'missing_parent' => 0 ], $errors); + $this->assertEquals(['oddness' => 0, + 'duplicates' => 0, + 'wrong_parent' => 0, + 'missing_parent' => 0], $errors); - Category::where('id', '=', 5)->update([ '_lft' => 14 ]); - Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]); - Category::where('id', '=', 11)->update([ '_lft' => 20 ]); - Category::where('id', '=', 4)->update([ 'parent_id' => 24 ]); + Category::where('id', '=', 5)->update(['_lft' => 14]); + Category::where('id', '=', 8)->update(['parent_id' => 2]); + Category::where('id', '=', 11)->update(['_lft' => 20]); + Category::where('id', '=', 4)->update(['parent_id' => 24]); $errors = Category::countErrors(); @@ -591,33 +595,32 @@ public function testCountsTreeErrors() $this->assertEquals(1, $errors['missing_parent']); } - public function testCreatesNode() + public function test_creates_node() { - $node = Category::create([ 'name' => 'test' ]); + $node = Category::create(['name' => 'test']); $this->assertEquals(23, $node->getLft()); } - public function testCreatesViaRelationship() + public function test_creates_via_relationship() { $node = $this->findCategory('apple'); - $child = $node->children()->create([ 'name' => 'test' ]); + $child = $node->children()->create(['name' => 'test']); $this->assertTreeNotBroken(); } - public function testCreatesTree() + public function test_creates_tree() { $node = Category::create( - [ - 'name' => 'test', - 'children' => [ - [ 'name' => 'test2' ], - [ 'name' => 'test3' ], - ], - ]); + 'name' => 'test', + 'children' => [ + ['name' => 'test2'], + ['name' => 'test3'], + ], + ]); $this->assertTreeNotBroken(); @@ -629,54 +632,53 @@ public function testCreatesTree() $this->assertEquals('test2', $node->children[0]->name); } - public function testDescendantsOfNonExistingNode() + public function test_descendants_of_non_existing_node() { $node = new Category; $this->assertTrue($node->getDescendants()->isEmpty()); } - public function testWhereDescendantsOf() + public function test_where_descendants_of() { - $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + $this->expectException(ModelNotFoundException::class); Category::whereDescendantOf(124)->get(); } - public function testAncestorsByNode() + public function test_ancestors_by_node() { $category = $this->findCategory('apple'); $ancestors = all(Category::whereAncestorOf($category)->pluck('id')); - $this->assertEquals([ 1, 2 ], $ancestors); + $this->assertEquals([1, 2], $ancestors); } - public function testDescendantsByNode() + public function test_descendants_by_node() { $category = $this->findCategory('notebooks'); $res = all(Category::whereDescendantOf($category)->pluck('id')); - $this->assertEquals([ 3, 4 ], $res); + $this->assertEquals([3, 4], $res); } - public function testMultipleDeletionsDoNotBrakeTree() + public function test_multiple_deletions_do_not_brake_tree() { $category = $this->findCategory('mobile'); - foreach ($category->children()->take(2)->get() as $child) - { + foreach ($category->children()->take(2)->get() as $child) { $child->forceDelete(); } $this->assertTreeNotBroken(); } - public function testTreeIsFixed() + public function test_tree_is_fixed() { - Category::where('id', '=', 5)->update([ '_lft' => 14 ]); - Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]); - Category::where('id', '=', 11)->update([ '_lft' => 20 ]); - Category::where('id', '=', 2)->update([ 'parent_id' => 24 ]); + Category::where('id', '=', 5)->update(['_lft' => 14]); + Category::where('id', '=', 8)->update(['parent_id' => 2]); + Category::where('id', '=', 11)->update(['_lft' => 20]); + Category::where('id', '=', 2)->update(['parent_id' => 24]); $fixed = Category::fixTree(); @@ -692,9 +694,9 @@ public function testTreeIsFixed() $this->assertEquals(null, $node->getParentId()); } - public function testSubtreeIsFixed() + public function test_subtree_is_fixed() { - Category::where('id', '=', 8)->update([ '_lft' => 11 ]); + Category::where('id', '=', 8)->update(['_lft' => 11]); $fixed = Category::fixSubtree(Category::find(5)); $this->assertEquals($fixed, 1); @@ -702,7 +704,7 @@ public function testSubtreeIsFixed() $this->assertEquals(Category::find(8)->getLft(), 12); } - public function testParentIdDirtiness() + public function test_parent_id_dirtiness() { $node = $this->findCategory('apple'); $node->parent_id = 5; @@ -715,7 +717,7 @@ public function testParentIdDirtiness() $this->assertTrue($node->isDirty('parent_id')); } - public function testIsDirtyMovement() + public function test_is_dirty_movement() { $node = $this->findCategory('apple'); $otherNode = $this->findCategory('samsung'); @@ -736,7 +738,7 @@ public function testIsDirtyMovement() $this->assertTrue($node->isDirty()); } - public function testRootNodesMoving() + public function test_root_nodes_moving() { $node = $this->findCategory('store'); $node->down(); @@ -744,7 +746,7 @@ public function testRootNodesMoving() $this->assertEquals(3, $node->getLft()); } - public function testDescendantsRelation() + public function test_descendants_relation() { $node = $this->findCategory('notebooks'); $result = $node->descendants; @@ -753,9 +755,9 @@ public function testDescendantsRelation() $this->assertEquals('apple', $result->first()->name); } - public function testDescendantsEagerlyLoaded() + public function test_descendants_eagerly_loaded() { - $nodes = Category::whereIn('id', [ 2, 5 ])->get(); + $nodes = Category::whereIn('id', [2, 5])->get(); $nodes->load('descendants'); @@ -763,9 +765,9 @@ public function testDescendantsEagerlyLoaded() $this->assertTrue($nodes->first()->relationLoaded('descendants')); } - public function testDescendantsRelationQuery() + public function test_descendants_relation_query() { - $nodes = Category::has('descendants')->whereIn('id', [ 2, 3 ])->get(); + $nodes = Category::has('descendants')->whereIn('id', [2, 3])->get(); $this->assertEquals(1, $nodes->count()); $this->assertEquals(2, $nodes->first()->getKey()); @@ -777,26 +779,26 @@ public function testDescendantsRelationQuery() $this->assertEquals(5, $nodes[1]->getKey()); } - public function testParentRelationQuery() + public function test_parent_relation_query() { - $nodes = Category::has('parent')->whereIn('id', [ 1, 2 ]); + $nodes = Category::has('parent')->whereIn('id', [1, 2]); $this->assertEquals(1, $nodes->count()); $this->assertEquals(2, $nodes->first()->getKey()); } - public function testRebuildTree() + public function test_rebuild_tree() { $fixed = Category::rebuildTree([ [ 'id' => 1, 'children' => [ - [ 'id' => 10 ], - [ 'id' => 3, 'name' => 'apple v2', 'children' => [ [ 'name' => 'new node' ] ] ], - [ 'id' => 2 ], + ['id' => 10], + ['id' => 3, 'name' => 'apple v2', 'children' => [['name' => 'new node']]], + ['id' => 2], - ] - ] + ], + ], ]); $this->assertTrue($fixed > 0); @@ -814,11 +816,11 @@ public function testRebuildTree() $this->assertEquals(3, $node->getParentId()); } - public function testRebuildSubtree() + public function test_rebuild_subtree() { $fixed = Category::rebuildSubtree(Category::find(7), [ - [ 'name' => 'new node' ], - [ 'id' => '8' ], + ['name' => 'new node'], + ['id' => '8'], ]); $this->assertTrue($fixed > 0); @@ -830,9 +832,9 @@ public function testRebuildSubtree() $this->assertEquals($node->getLft(), 12); } - public function testRebuildTreeWithDeletion() + public function test_rebuild_tree_with_deletion() { - Category::rebuildTree([ [ 'name' => 'all deleted' ] ], true); + Category::rebuildTree([['name' => 'all deleted']], true); $this->assertTreeNotBroken(); @@ -846,14 +848,14 @@ public function testRebuildTreeWithDeletion() $this->assertTrue($nodes->count() > 1); } - public function testRebuildFailsWithInvalidPK() + public function test_rebuild_fails_with_invalid_pk() { - $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + $this->expectException(ModelNotFoundException::class); - Category::rebuildTree([ [ 'id' => 24 ] ]); + Category::rebuildTree([['id' => 24]]); } - public function testFlatTree() + public function test_flat_tree() { $node = $this->findCategory('mobile'); $tree = $node->descendants()->orderBy('name')->get()->toFlatTree(); @@ -880,7 +882,7 @@ public function testFlatTree() $duplicate->saveAsRoot(); }*/ - public function testWhereIsLeaf() + public function test_where_is_leaf() { $categories = Category::leaves(); @@ -893,7 +895,7 @@ public function testWhereIsLeaf() $this->assertFalse($category->isLeaf()); } - public function testEagerLoadAncestors() + public function test_eager_load_ancestors() { $queryLogCount = count(Capsule::connection()->getQueryLog()); $categories = Category::with('ancestors')->orderBy('name')->get(); @@ -911,21 +913,23 @@ public function testEagerLoadAncestors() 'samsung (7)}' => 'store (1) > mobile (5)', 'sony (9)}' => 'store (1) > mobile (5)', 'store (1)}' => '', - 'store_2 (11)}' => '' + 'store_2 (11)}' => '', ]; $output = []; foreach ($categories as $category) { $output["{$category->name} ({$category->id})}"] = $category->ancestors->count() - ? implode(' > ', $category->ancestors->map(function ($cat) { return "{$cat->name} ({$cat->id})"; })->toArray()) + ? implode(' > ', $category->ancestors->map(function ($cat) { + return "{$cat->name} ({$cat->id})"; + })->toArray()) : ''; } $this->assertEquals($expectedShape, $output); } - public function testLazyLoadAncestors() + public function test_lazy_load_ancestors() { $queryLogCount = count(Capsule::connection()->getQueryLog()); $categories = Category::orderBy('name')->get(); @@ -943,14 +947,16 @@ public function testLazyLoadAncestors() 'samsung (7)}' => 'store (1) > mobile (5)', 'sony (9)}' => 'store (1) > mobile (5)', 'store (1)}' => '', - 'store_2 (11)}' => '' + 'store_2 (11)}' => '', ]; $output = []; foreach ($categories as $category) { $output["{$category->name} ({$category->id})}"] = $category->ancestors->count() - ? implode(' > ', $category->ancestors->map(function ($cat) { return "{$cat->name} ({$cat->id})"; })->toArray()) + ? implode(' > ', $category->ancestors->map(function ($cat) { + return "{$cat->name} ({$cat->id})"; + })->toArray()) : ''; } @@ -960,20 +966,20 @@ public function testLazyLoadAncestors() $this->assertEquals($expectedShape, $output); } - public function testWhereHasCountQueryForAncestors() + public function test_where_has_count_query_for_ancestors() { $categories = all(Category::has('ancestors', '>', 2)->pluck('name')); - $this->assertEquals([ 'galaxy' ], $categories); + $this->assertEquals(['galaxy'], $categories); $categories = all(Category::whereHas('ancestors', function ($query) { $query->where('id', 5); })->pluck('name')); - $this->assertEquals([ 'nokia', 'samsung', 'galaxy', 'sony', 'lenovo' ], $categories); + $this->assertEquals(['nokia', 'samsung', 'galaxy', 'sony', 'lenovo'], $categories); } - public function testReplication() + public function test_replication() { $category = $this->findCategory('nokia'); $category = $category->replicate(); @@ -992,13 +998,12 @@ public function testReplication() $this->assertEquals(1, $category->getParentId()); } - public function testIsNodeDetectsTraitUsers() + public function test_is_node_detects_trait_users() { $this->assertTrue(NestedSet::isNode(new Category)); - $this->assertFalse(NestedSet::isNode(new \stdClass)); + $this->assertFalse(NestedSet::isNode(new stdClass)); $this->assertFalse(NestedSet::isNode('not an object')); } - } function all($items) diff --git a/tests/ScopedNodeTest.php b/tests/ScopedNodeTest.php index 2da34b9..340067c 100644 --- a/tests/ScopedNodeTest.php +++ b/tests/ScopedNodeTest.php @@ -1,9 +1,12 @@ create('menu_items', function (\Illuminate\Database\Schema\Blueprint $table) { + $schema->create('menu_items', function (Blueprint $table) { $table->increments('id'); $table->unsignedInteger('menu_id'); $table->string('title')->nullable(); @@ -23,7 +26,7 @@ public static function setUpBeforeClass(): void Capsule::enableQueryLog(); } - public function setUp(): void + protected function setUp(): void { $data = include __DIR__.'/data/menu_items.php'; @@ -36,23 +39,23 @@ public function setUp(): void date_default_timezone_set('America/Denver'); } - public function tearDown(): void + protected function tearDown(): void { Capsule::table('menu_items')->truncate(); } public function assertTreeNotBroken($menuId) { - $this->assertFalse(MenuItem::scoped([ 'menu_id' => $menuId ])->isBroken()); + $this->assertFalse(MenuItem::scoped(['menu_id' => $menuId])->isBroken()); } - public function testNotBroken() + public function test_not_broken() { $this->assertTreeNotBroken(1); $this->assertTreeNotBroken(2); } - public function testMovingNodeNotAffectingOtherMenu() + public function test_moving_node_not_affecting_other_menu() { $node = MenuItem::where('menu_id', '=', 1)->first(); @@ -63,14 +66,14 @@ public function testMovingNodeNotAffectingOtherMenu() $this->assertEquals(1, $node->getLft()); } - public function testScoped() + public function test_scoped() { - $node = MenuItem::scoped([ 'menu_id' => 2 ])->first(); + $node = MenuItem::scoped(['menu_id' => 2])->first(); $this->assertEquals(3, $node->getKey()); } - public function testSiblings() + public function test_siblings() { $node = MenuItem::find(1); @@ -90,7 +93,7 @@ public function testSiblings() $this->assertEquals(1, $result->first()->getKey()); } - public function testDescendants() + public function test_descendants() { $node = MenuItem::find(2); @@ -99,7 +102,7 @@ public function testDescendants() $this->assertEquals(1, $result->count()); $this->assertEquals(5, $result->first()->getKey()); - $node = MenuItem::scoped([ 'menu_id' => 1 ])->with('descendants')->find(2); + $node = MenuItem::scoped(['menu_id' => 1])->with('descendants')->find(2); $result = $node->descendants; @@ -107,7 +110,7 @@ public function testDescendants() $this->assertEquals(5, $result->first()->getKey()); } - public function testAncestors() + public function test_ancestors() { $node = MenuItem::find(5); @@ -116,7 +119,7 @@ public function testAncestors() $this->assertEquals(1, $result->count()); $this->assertEquals(2, $result->first()->getKey()); - $node = MenuItem::scoped([ 'menu_id' => 1 ])->with('ancestors')->find(5); + $node = MenuItem::scoped(['menu_id' => 1])->with('ancestors')->find(5); $result = $node->ancestors; @@ -124,9 +127,9 @@ public function testAncestors() $this->assertEquals(2, $result->first()->getKey()); } - public function testDepth() + public function test_depth() { - $node = MenuItem::scoped([ 'menu_id' => 1 ])->withDepth()->where('id', '=', 5)->first(); + $node = MenuItem::scoped(['menu_id' => 1])->withDepth()->where('id', '=', 5)->first(); $this->assertEquals(1, $node->depth); @@ -137,7 +140,7 @@ public function testDepth() $this->assertEquals(1, $result->first()->depth); } - public function testSaveAsRoot() + public function test_save_as_root() { $node = MenuItem::find(5); @@ -149,9 +152,9 @@ public function testSaveAsRoot() $this->assertOtherScopeNotAffected(); } - public function testInsertion() + public function test_insertion() { - $node = MenuItem::create([ 'menu_id' => 1, 'parent_id' => 5 ]); + $node = MenuItem::create(['menu_id' => 1, 'parent_id' => 5]); $this->assertEquals(5, $node->parent_id); $this->assertEquals(5, $node->getLft()); @@ -159,14 +162,14 @@ public function testInsertion() $this->assertOtherScopeNotAffected(); } - public function testInsertionToParentFromOtherScope() + public function test_insertion_to_parent_from_other_scope() { - $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + $this->expectException(ModelNotFoundException::class); - $node = MenuItem::create([ 'menu_id' => 2, 'parent_id' => 5 ]); + $node = MenuItem::create(['menu_id' => 2, 'parent_id' => 5]); } - public function testDeletion() + public function test_deletion() { $node = MenuItem::find(2)->delete(); @@ -177,7 +180,7 @@ public function testDeletion() $this->assertOtherScopeNotAffected(); } - public function testMoving() + public function test_moving() { $node = MenuItem::find(1); $this->assertTrue($node->down()); @@ -200,7 +203,7 @@ protected function assertOtherScopeNotAffected() MenuItem::scoped([ 'menu_id' => 2 ])->rebuildTree($data); }*/ - public function testAppendingToAnotherScopeFails() + public function test_appending_to_another_scope_fails() { $this->expectException(LogicException::class); @@ -210,7 +213,7 @@ public function testAppendingToAnotherScopeFails() $a->appendToNode($b)->save(); } - public function testInsertingBeforeAnotherScopeFails() + public function test_inserting_before_another_scope_fails() { $this->expectException(LogicException::class); @@ -220,7 +223,7 @@ public function testInsertingBeforeAnotherScopeFails() $a->insertAfterNode($b); } - public function testEagerLoadingAncestorsWithScope() + public function test_eager_loading_ancestors_with_scope() { $filteredNodes = MenuItem::where('title', 'menu item 3')->with(['ancestors'])->get(); @@ -228,11 +231,11 @@ public function testEagerLoadingAncestorsWithScope() $this->assertEquals(4, $filteredNodes->find(6)->ancestors[0]->id); } - public function testEagerLoadingDescendantsWithScope() + public function test_eager_loading_descendants_with_scope() { $filteredNodes = MenuItem::where('title', 'menu item 2')->with(['descendants'])->get(); $this->assertEquals(5, $filteredNodes->find(2)->descendants[0]->id); $this->assertEquals(6, $filteredNodes->find(4)->descendants[0]->id); } -} \ No newline at end of file +} diff --git a/tests/data/categories.php b/tests/data/categories.php index 1f5b8ab..7ba0d14 100644 --- a/tests/data/categories.php +++ b/tests/data/categories.php @@ -1,15 +1,15 @@ 1, 'name' => 'store', '_lft' => 1, '_rgt' => 20, 'parent_id' => null), - array('id' => 2, 'name' => 'notebooks', '_lft' => 2, '_rgt' => 7, 'parent_id' => 1), - array('id' => 3, 'name' => 'apple', '_lft' => 3, '_rgt' => 4, 'parent_id' => 2), - array('id' => 4, 'name' => 'lenovo', '_lft' => 5, '_rgt' => 6, 'parent_id' => 2), - array('id' => 5, 'name' => 'mobile', '_lft' => 8, '_rgt' => 19, 'parent_id' => 1), - array('id' => 6, 'name' => 'nokia', '_lft' => 9, '_rgt' => 10, 'parent_id' => 5), - array('id' => 7, 'name' => 'samsung', '_lft' => 11, '_rgt' => 14, 'parent_id' => 5), - array('id' => 8, 'name' => 'galaxy', '_lft' => 12, '_rgt' => 13, 'parent_id' => 7), - array('id' => 9, 'name' => 'sony', '_lft' => 15, '_rgt' => 16, 'parent_id' => 5), - array('id' => 10, 'name' => 'lenovo', '_lft' => 17, '_rgt' => 18, 'parent_id' => 5), - array('id' => 11, 'name' => 'store_2', '_lft' => 21, '_rgt' => 22, 'parent_id' => null), -); \ No newline at end of file +return [ + ['id' => 1, 'name' => 'store', '_lft' => 1, '_rgt' => 20, 'parent_id' => null], + ['id' => 2, 'name' => 'notebooks', '_lft' => 2, '_rgt' => 7, 'parent_id' => 1], + ['id' => 3, 'name' => 'apple', '_lft' => 3, '_rgt' => 4, 'parent_id' => 2], + ['id' => 4, 'name' => 'lenovo', '_lft' => 5, '_rgt' => 6, 'parent_id' => 2], + ['id' => 5, 'name' => 'mobile', '_lft' => 8, '_rgt' => 19, 'parent_id' => 1], + ['id' => 6, 'name' => 'nokia', '_lft' => 9, '_rgt' => 10, 'parent_id' => 5], + ['id' => 7, 'name' => 'samsung', '_lft' => 11, '_rgt' => 14, 'parent_id' => 5], + ['id' => 8, 'name' => 'galaxy', '_lft' => 12, '_rgt' => 13, 'parent_id' => 7], + ['id' => 9, 'name' => 'sony', '_lft' => 15, '_rgt' => 16, 'parent_id' => 5], + ['id' => 10, 'name' => 'lenovo', '_lft' => 17, '_rgt' => 18, 'parent_id' => 5], + ['id' => 11, 'name' => 'store_2', '_lft' => 21, '_rgt' => 22, 'parent_id' => null], +]; diff --git a/tests/data/menu_items.php b/tests/data/menu_items.php index 5490f7d..2d0d990 100644 --- a/tests/data/menu_items.php +++ b/tests/data/menu_items.php @@ -1,8 +1,10 @@ - 1, 'menu_id' => 1, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1' ], - [ 'id' => 2, 'menu_id' => 1, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2' ], - [ 'id' => 5, 'menu_id' => 1, '_lft' => 4, '_rgt' => 5, 'parent_id' => 2, 'title' => 'menu item 3' ], - [ 'id' => 3, 'menu_id' => 2, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1' ], - [ 'id' => 4, 'menu_id' => 2, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2' ], - [ 'id' => 6, 'menu_id' => 2, '_lft' => 4, '_rgt' => 5, 'parent_id' => 4, 'title' => 'menu item 3' ], -]; \ No newline at end of file + 1, 'menu_id' => 1, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1'], + ['id' => 2, 'menu_id' => 1, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2'], + ['id' => 5, 'menu_id' => 1, '_lft' => 4, '_rgt' => 5, 'parent_id' => 2, 'title' => 'menu item 3'], + ['id' => 3, 'menu_id' => 2, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1'], + ['id' => 4, 'menu_id' => 2, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2'], + ['id' => 6, 'menu_id' => 2, '_lft' => 4, '_rgt' => 5, 'parent_id' => 4, 'title' => 'menu item 3'], +]; diff --git a/tests/models/Category.php b/tests/models/Category.php index 12d472c..5483b90 100644 --- a/tests/models/Category.php +++ b/tests/models/Category.php @@ -1,12 +1,13 @@ Date: Mon, 1 Jun 2026 14:13:26 +0100 Subject: [PATCH 3/7] Migrate test suite to Orchestra Testbench Replace the hand-rolled Capsule bootstrap with Orchestra Testbench so the package is exercised through a real Laravel application: service provider registration, the database layer, and connection config. - Namespace tests and models under Lunar\Nestedset\Tests (PSR-4 autoload-dev) - Add a base TestCase that configures sqlite/mysql/pgsql connections from env vars, ready for a multi-engine CI matrix - Reset Postgres sequences after seeding rows with explicit ids - Add ServiceProviderTest covering the nestedSet()/dropNestedSet() macros - Drop phpunit.php; bootstrap from the Composer autoloader Co-Authored-By: Claude Opus 4.8 (1M context) --- composer.json | 8 ++- phpunit.php | 15 ---- phpunit.xml | 6 +- tests/{models => Models}/Category.php | 5 +- .../{models => Models}/DuplicateCategory.php | 2 + tests/{models => Models}/MenuItem.php | 2 + tests/NodeTest.php | 58 ++++++--------- tests/ScopedNodeTest.php | 35 ++++----- tests/ServiceProviderTest.php | 58 +++++++++++++++ tests/TestCase.php | 72 +++++++++++++++++++ 10 files changed, 185 insertions(+), 76 deletions(-) delete mode 100644 phpunit.php rename tests/{models => Models}/Category.php (75%) rename tests/{models => Models}/DuplicateCategory.php (85%) rename tests/{models => Models}/MenuItem.php (90%) create mode 100644 tests/ServiceProviderTest.php create mode 100644 tests/TestCase.php diff --git a/composer.json b/composer.json index 29bd5f7..512be35 100644 --- a/composer.json +++ b/composer.json @@ -31,9 +31,15 @@ "Lunar\\Nestedset\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Lunar\\Nestedset\\Tests\\": "tests/" + } + }, "require-dev": { "phpunit/phpunit": "^11.0", - "laravel/pint": "^1.29" + "laravel/pint": "^1.29", + "orchestra/testbench": "^10.0|^11.0" }, "minimum-stability": "stable", "prefer-stable": true, diff --git a/phpunit.php b/phpunit.php deleted file mode 100644 index e1ca400..0000000 --- a/phpunit.php +++ /dev/null @@ -1,15 +0,0 @@ -addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'prfx_']); -$capsule->setEventDispatcher(new Dispatcher); -$capsule->bootEloquent(); -$capsule->setAsGlobal(); - -include __DIR__.'/tests/models/Category.php'; -include __DIR__.'/tests/models/MenuItem.php'; diff --git a/phpunit.xml b/phpunit.xml index fac2e3f..6dd80c7 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,10 +1,8 @@ - + - ./tests/ - ./tests/data - ./tests/models + ./tests diff --git a/tests/models/Category.php b/tests/Models/Category.php similarity index 75% rename from tests/models/Category.php rename to tests/Models/Category.php index 5483b90..e426ff9 100644 --- a/tests/models/Category.php +++ b/tests/Models/Category.php @@ -1,11 +1,14 @@ dropIfExists('categories'); + parent::setUp(); - Capsule::disableQueryLog(); + Schema::dropIfExists('categories'); - $schema->create('categories', function (Blueprint $table) { + Schema::create('categories', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->softDeletes(); NestedSet::columns($table); }); - Capsule::enableQueryLog(); - } - - protected function setUp(): void - { $data = include __DIR__.'/data/categories.php'; - Capsule::table('categories')->insert($data); + DB::table('categories')->insert($data); + + $this->resetAutoIncrement('categories'); - Capsule::flushQueryLog(); + DB::connection()->enableQueryLog(); + DB::connection()->flushQueryLog(); Category::resetActionsPerformed(); date_default_timezone_set('America/Denver'); } - protected function tearDown(): void - { - Capsule::table('categories')->truncate(); - } - - // public static function tearDownAfterClass() - // { - // $log = Capsule::getQueryLog(); - // foreach ($log as $item) { - // echo $item['query']." with ".implode(', ', $item['bindings'])."\n"; - // } - // } - public function assertTreeNotBroken($table = 'categories') { $checks = []; - $connection = Capsule::connection(); + $connection = DB::connection(); $table = $connection->getQueryGrammar()->wrapTable($table); @@ -81,7 +69,7 @@ public function assertTreeNotBroken($table = 'categories') $actual = $connection->selectOne($sql); $this->assertEquals(null, $actual->errors, "The tree structure of $table is broken!"); - $actual = (array) Capsule::connection()->selectOne($sql); + $actual = (array) DB::connection()->selectOne($sql); $this->assertEquals(['errors' => null], $actual, "The tree structure of $table is broken!"); } @@ -897,10 +885,10 @@ public function test_where_is_leaf() public function test_eager_load_ancestors() { - $queryLogCount = count(Capsule::connection()->getQueryLog()); + $queryLogCount = count(DB::connection()->getQueryLog()); $categories = Category::with('ancestors')->orderBy('name')->get(); - $this->assertEquals($queryLogCount + 2, count(Capsule::connection()->getQueryLog())); + $this->assertEquals($queryLogCount + 2, count(DB::connection()->getQueryLog())); $expectedShape = [ 'apple (3)}' => 'store (1) > notebooks (2)', @@ -931,10 +919,10 @@ public function test_eager_load_ancestors() public function test_lazy_load_ancestors() { - $queryLogCount = count(Capsule::connection()->getQueryLog()); + $queryLogCount = count(DB::connection()->getQueryLog()); $categories = Category::orderBy('name')->get(); - $this->assertEquals($queryLogCount + 1, count(Capsule::connection()->getQueryLog())); + $this->assertEquals($queryLogCount + 1, count(DB::connection()->getQueryLog())); $expectedShape = [ 'apple (3)}' => 'store (1) > notebooks (2)', @@ -961,7 +949,7 @@ public function test_lazy_load_ancestors() } // assert that there is number of original query + 1 + number of rows to fulfill the relation - $this->assertEquals($queryLogCount + 12, count(Capsule::connection()->getQueryLog())); + $this->assertEquals($queryLogCount + 12, count(DB::connection()->getQueryLog())); $this->assertEquals($expectedShape, $output); } diff --git a/tests/ScopedNodeTest.php b/tests/ScopedNodeTest.php index 340067c..500caa9 100644 --- a/tests/ScopedNodeTest.php +++ b/tests/ScopedNodeTest.php @@ -1,49 +1,44 @@ dropIfExists('menu_items'); + parent::setUp(); - Capsule::disableQueryLog(); + Schema::dropIfExists('menu_items'); - $schema->create('menu_items', function (Blueprint $table) { + Schema::create('menu_items', function (Blueprint $table) { $table->increments('id'); $table->unsignedInteger('menu_id'); $table->string('title')->nullable(); NestedSet::columns($table); }); - Capsule::enableQueryLog(); - } - - protected function setUp(): void - { $data = include __DIR__.'/data/menu_items.php'; - Capsule::table('menu_items')->insert($data); + DB::table('menu_items')->insert($data); + + $this->resetAutoIncrement('menu_items'); - Capsule::flushQueryLog(); + DB::connection()->enableQueryLog(); + DB::connection()->flushQueryLog(); MenuItem::resetActionsPerformed(); date_default_timezone_set('America/Denver'); } - protected function tearDown(): void - { - Capsule::table('menu_items')->truncate(); - } - public function assertTreeNotBroken($menuId) { $this->assertFalse(MenuItem::scoped(['menu_id' => $menuId])->isBroken()); diff --git a/tests/ServiceProviderTest.php b/tests/ServiceProviderTest.php new file mode 100644 index 0000000..ef4419b --- /dev/null +++ b/tests/ServiceProviderTest.php @@ -0,0 +1,58 @@ +increments('id'); + $table->nestedSet(); + }); + + foreach (NestedSet::getDefaultColumns() as $column) { + $this->assertTrue( + Schema::hasColumn('macro_nodes', $column), + "Expected column [$column] to be created by the nestedSet() macro." + ); + } + } + + public function test_drop_nested_set_blueprint_macro_removes_columns() + { + Schema::create('macro_nodes', function (Blueprint $table) { + $table->increments('id'); + $table->nestedSet(); + }); + + Schema::table('macro_nodes', function (Blueprint $table) { + $table->dropNestedSet(); + }); + + foreach (NestedSet::getDefaultColumns() as $column) { + $this->assertFalse( + Schema::hasColumn('macro_nodes', $column), + "Expected column [$column] to be removed by the dropNestedSet() macro." + ); + } + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..8d376e3 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,72 @@ +set('database.default', $connection); + + $app['config']->set('database.connections.testbench', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => 'prfx_', + ]); + + $app['config']->set('database.connections.mysql', [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'nestedset'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'prefix' => 'prfx_', + ]); + + $app['config']->set('database.connections.pgsql', [ + 'driver' => 'pgsql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'nestedset'), + 'username' => env('DB_USERNAME', 'postgres'), + 'password' => env('DB_PASSWORD', ''), + 'prefix' => 'prfx_', + ]); + } + + /** + * Realign an auto-increment sequence after seeding rows with explicit ids. + * + * MySQL and SQLite advance the sequence automatically; PostgreSQL does not. + */ + protected function resetAutoIncrement(string $table): void + { + $connection = DB::connection(); + + if ($connection->getDriverName() !== 'pgsql') { + return; + } + + $prefixed = $connection->getTablePrefix().$table; + $max = (int) $connection->table($table)->max('id'); + + $connection->statement( + "SELECT setval(pg_get_serial_sequence(?, 'id'), ?)", + [$prefixed, max($max, 1)] + ); + } +} From bac4fc22f64fb77a7bfc12f5ddc379004eb95cf2 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Mon, 1 Jun 2026 14:19:43 +0100 Subject: [PATCH 4/7] Test against SQLite, MySQL and PostgreSQL in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run the suite across all three engines via service containers, and add a dedicated coverage job (pcov + clover). Validated locally against MySQL 8 and PostgreSQL 16. Fix a test that ordered categories by a non-existent `title` column: SQLite and MySQL tolerate it, but PostgreSQL (correctly) rejects it. Order by `name` instead — the assertions are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/run-tests.yml | 83 ++++++++++++++++++++++++++++++++- tests/NodeTest.php | 2 +- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 894756b..c099af3 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -27,6 +27,34 @@ jobs: name: PHP${{ matrix.php }} - Laravel${{ matrix.laravel }} + services: + mysql: + image: mysql:8 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: true + MYSQL_DATABASE: nestedset + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping --silent" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + postgres: + image: postgres:16 + env: + POSTGRES_DB: nestedset + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -35,6 +63,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + extensions: pdo_sqlite, pdo_mysql, pdo_pgsql coverage: none tools: composer:v2 @@ -43,5 +72,57 @@ jobs: 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 + - name: Tests (SQLite) + run: composer test + env: + DB_CONNECTION: testbench + + - name: Tests (MySQL) run: composer test + env: + DB_CONNECTION: mysql + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_DATABASE: nestedset + DB_USERNAME: root + DB_PASSWORD: '' + + - name: Tests (PostgreSQL) + run: composer test + env: + DB_CONNECTION: pgsql + DB_HOST: 127.0.0.1 + DB_PORT: 5432 + DB_DATABASE: nestedset + DB_USERNAME: postgres + DB_PASSWORD: password + + coverage: + runs-on: ubuntu-latest + + timeout-minutes: 15 + + env: + COMPOSER_NO_INTERACTION: 1 + + name: Coverage + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: pdo_sqlite + coverage: pcov + tools: composer:v2 + + - name: Install dependencies + run: composer update -o --quiet --prefer-dist + + - name: Execute tests with coverage + run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml + env: + DB_CONNECTION: testbench diff --git a/tests/NodeTest.php b/tests/NodeTest.php index d4b21eb..eb1909e 100644 --- a/tests/NodeTest.php +++ b/tests/NodeTest.php @@ -451,7 +451,7 @@ public function test_to_tree_builds_with_default_order() public function test_to_tree_builds_with_custom_order() { $tree = Category::whereBetween('_lft', [8, 17]) - ->orderBy('title') + ->orderBy('name') ->get() ->toTree(); From 9172111986121e19bcf4df51d719c7d149899276 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Mon, 1 Jun 2026 14:32:46 +0100 Subject: [PATCH 5/7] Add native type declarations across src Add parameter, return and property types throughout the library now that the package requires PHP 8.3+, and remove docblocks made redundant by them. No behavioural change. Framework-override signatures are kept compatible with the installed Laravel version (some params left untyped where the parent declares none). A few inaccurate upstream docblocks were corrected to match real return types (e.g. moveNode returns bool, columnPatch returns Expression, getMissingParentQuery returns a base query builder). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AncestorsRelation.php | 21 +- src/BaseRelation.php | 48 +--- src/Collection.php | 25 +- src/DescendantsRelation.php | 20 +- src/NestedSet.php | 13 +- src/NestedSetServiceProvider.php | 2 +- src/NodeTrait.php | 394 ++++++++----------------------- src/QueryBuilder.php | 265 +++++---------------- 8 files changed, 179 insertions(+), 609 deletions(-) diff --git a/src/AncestorsRelation.php b/src/AncestorsRelation.php index 73bc910..a611466 100644 --- a/src/AncestorsRelation.php +++ b/src/AncestorsRelation.php @@ -8,10 +8,8 @@ class AncestorsRelation extends BaseRelation { /** * Set the base constraints on the relation query. - * - * @return void */ - public function addConstraints() + public function addConstraints(): void { if (! static::$constraints) { return; @@ -21,28 +19,17 @@ public function addConstraints() ->applyNestedSetScope(); } - /** - * @return bool - */ - protected function matches(Model $model, $related) + protected function matches(Model $model, Model $related): bool { return $related->isAncestorOf($model); } - /** - * @param QueryBuilder $query - * @param Model $model - * @return void - */ - protected function addEagerConstraint($query, $model) + protected function addEagerConstraint(QueryBuilder $query, Model $model): void { $query->orWhereAncestorOf($model); } - /** - * @return string - */ - protected function relationExistenceCondition($hash, $table, $lft, $rgt) + protected function relationExistenceCondition(string $hash, string $table, string $lft, string $rgt): string { $key = $this->getBaseQuery()->getGrammar()->wrap($this->parent->getKeyName()); diff --git a/src/BaseRelation.php b/src/BaseRelation.php index 0f2347d..e8b28f0 100644 --- a/src/BaseRelation.php +++ b/src/BaseRelation.php @@ -40,27 +40,12 @@ public function __construct(QueryBuilder $builder, Model $model) parent::__construct($builder, $model); } - /** - * @return bool - */ - abstract protected function matches(Model $model, $related); + abstract protected function matches(Model $model, Model $related): bool; - /** - * @param QueryBuilder $query - * @param Model $model - * @return void - */ - abstract protected function addEagerConstraint($query, $model); + abstract protected function addEagerConstraint(QueryBuilder $query, Model $model): void; - /** - * @return string - */ - abstract protected function relationExistenceCondition($hash, $table, $lft, $rgt); + abstract protected function relationExistenceCondition(string $hash, string $table, string $lft, string $rgt): string; - /** - * @param array $columns - * @return mixed - */ public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilder $parent, $columns = ['*'] ) { @@ -87,9 +72,8 @@ public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilde * Initialize the relation on a set of models. * * @param string $relation - * @return array */ - public function initRelation(array $models, $relation) + public function initRelation(array $models, $relation): array { return $models; } @@ -98,9 +82,8 @@ public function initRelation(array $models, $relation) * Get a relationship join table hash. * * @param bool $incrementJoinCount - * @return string */ - public function getRelationCountHash($incrementJoinCount = true) + public function getRelationCountHash($incrementJoinCount = true): string { return 'nested_set_'.($incrementJoinCount ? static::$selfJoinCount++ : static::$selfJoinCount); } @@ -117,11 +100,8 @@ public function getResults() /** * Set the constraints for an eager load of the relation. - * - * - * @return void */ - public function addEagerConstraints(array $models) + public function addEagerConstraints(array $models): void { $this->query->whereNested(function (Builder $inner) use ($models) { // We will use this query in order to apply constraints to the @@ -138,9 +118,8 @@ public function addEagerConstraints(array $models) * Match the eagerly loaded results to their parents. * * @param string $relation - * @return array */ - public function match(array $models, EloquentCollection $results, $relation) + public function match(array $models, EloquentCollection $results, $relation): array { foreach ($models as $model) { $related = $this->matchForModel($model, $results); @@ -151,10 +130,7 @@ public function match(array $models, EloquentCollection $results, $relation) return $models; } - /** - * @return Collection - */ - protected function matchForModel(Model $model, EloquentCollection $results) + protected function matchForModel(Model $model, EloquentCollection $results): EloquentCollection { $result = $this->related->newCollection(); @@ -169,10 +145,8 @@ protected function matchForModel(Model $model, EloquentCollection $results) /** * Get the plain foreign key. - * - * @return mixed */ - public function getForeignKeyName() + public function getForeignKeyName(): string { // Return a stub value for relation // resolvers which need this function. @@ -181,10 +155,8 @@ public function getForeignKeyName() /** * Get the Qualify plain foreign key. - * - * @return mixed */ - public function getQualifiedForeignKeyName() + public function getQualifiedForeignKeyName(): string { // Return a stub value for relation // resolvers which need this function. diff --git a/src/Collection.php b/src/Collection.php index 0f90017..5df1e8b 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -11,10 +11,8 @@ class Collection extends BaseCollection * Fill `parent` and `children` relationships for every node in the collection. * * This will overwrite any previously set relations. - * - * @return $this */ - public function linkNodes() + public function linkNodes(): static { if ($this->isEmpty()) { return $this; @@ -47,11 +45,8 @@ public function linkNodes() * To successfully build tree "id", "_lft" and "parent_id" keys must present. * * If `$root` is provided, the tree will contain only descendants of that node. - * - * @param mixed $root - * @return Collection */ - public function toTree($root = false) + public function toTree(mixed $root = false): static { if ($this->isEmpty()) { return new static; @@ -73,11 +68,7 @@ public function toTree($root = false) return new static($items); } - /** - * @param mixed $root - * @return int - */ - protected function getRootNodeId($root = false) + protected function getRootNodeId(mixed $root = false): mixed { if (NestedSet::isNode($root)) { return $root->getKey(); @@ -105,11 +96,8 @@ protected function getRootNodeId($root = false) /** * Build a list of nodes that retain the order that they were pulled from * the database. - * - * @param bool $root - * @return static */ - public function toFlatTree($root = false) + public function toFlatTree(mixed $root = false): static { $result = new static; @@ -124,11 +112,8 @@ public function toFlatTree($root = false) /** * Flatten a tree into a non recursive array. - * - * @param mixed $parentId - * @return $this */ - protected function flattenTree(self $groupedNodes, $parentId) + protected function flattenTree(self $groupedNodes, mixed $parentId): static { foreach ($groupedNodes->get($parentId, []) as $node) { $this->push($node); diff --git a/src/DescendantsRelation.php b/src/DescendantsRelation.php index 07c5ff8..56a5b19 100644 --- a/src/DescendantsRelation.php +++ b/src/DescendantsRelation.php @@ -8,10 +8,8 @@ class DescendantsRelation extends BaseRelation { /** * Set the base constraints on the relation query. - * - * @return void */ - public function addConstraints() + public function addConstraints(): void { if (! static::$constraints) { return; @@ -21,27 +19,17 @@ public function addConstraints() ->applyNestedSetScope(); } - /** - * @param QueryBuilder $query - * @param Model $model - */ - protected function addEagerConstraint($query, $model) + protected function addEagerConstraint(QueryBuilder $query, Model $model): void { $query->orWhereDescendantOf($model); } - /** - * @return mixed - */ - protected function matches(Model $model, $related) + protected function matches(Model $model, Model $related): bool { return $related->isDescendantOf($model); } - /** - * @return string - */ - protected function relationExistenceCondition($hash, $table, $lft, $rgt) + protected function relationExistenceCondition(string $hash, string $table, string $lft, string $rgt): string { return "{$hash}.{$lft} between {$table}.{$lft} + 1 and {$table}.{$rgt}"; } diff --git a/src/NestedSet.php b/src/NestedSet.php index cdc9e27..45b5856 100644 --- a/src/NestedSet.php +++ b/src/NestedSet.php @@ -34,7 +34,7 @@ class NestedSet /** * Add default nested set columns to the table. Also create an index. */ - public static function columns(Blueprint $table) + public static function columns(Blueprint $table): void { $table->unsignedInteger(self::LFT)->default(0); $table->unsignedInteger(self::RGT)->default(0); @@ -46,7 +46,7 @@ public static function columns(Blueprint $table) /** * Drop NestedSet columns. */ - public static function dropColumns(Blueprint $table) + public static function dropColumns(Blueprint $table): void { $columns = static::getDefaultColumns(); @@ -57,20 +57,17 @@ public static function dropColumns(Blueprint $table) /** * Get a list of default columns. * - * @return array + * @return array */ - public static function getDefaultColumns() + public static function getDefaultColumns(): array { return [static::LFT, static::RGT, static::PARENT_ID]; } /** * Replaces instanceof calls for this trait. - * - * @param mixed $node - * @return bool */ - public static function isNode($node) + public static function isNode(mixed $node): bool { return is_object($node) && in_array(NodeTrait::class, class_uses_recursive($node), true); } diff --git a/src/NestedSetServiceProvider.php b/src/NestedSetServiceProvider.php index 10cd039..32f8540 100644 --- a/src/NestedSetServiceProvider.php +++ b/src/NestedSetServiceProvider.php @@ -7,7 +7,7 @@ class NestedSetServiceProvider extends ServiceProvider { - public function register() + public function register(): void { Blueprint::macro('nestedSet', function () { NestedSet::columns($this); diff --git a/src/NodeTrait.php b/src/NodeTrait.php index 93b02f6..c72ed87 100644 --- a/src/NodeTrait.php +++ b/src/NodeTrait.php @@ -16,34 +16,25 @@ trait NodeTrait { /** * Pending operation. - * - * @var array */ - protected $pending; + protected ?array $pending = null; /** * Whether the node has moved since last save. - * - * @var bool */ - protected $moved = false; + protected bool $moved = false; - /** - * @var Carbon - */ - public static $deletedAt; + public static ?Carbon $deletedAt; /** * Keep track of the number of performed operations. - * - * @var int */ - public static $actionsPerformed = 0; + public static int $actionsPerformed = 0; /** * Sign on model events. */ - public static function bootNodeTrait() + public static function bootNodeTrait(): void { static::whenBooted(function () { static::saving(function ($model) { @@ -73,11 +64,8 @@ public static function bootNodeTrait() /** * Set an action. - * - * @param string $action - * @return $this */ - protected function setNodeAction($action) + protected function setNodeAction(string $action): static { $this->pending = func_get_args(); @@ -87,7 +75,7 @@ protected function setNodeAction($action) /** * Call pending action. */ - protected function callPendingAction() + protected function callPendingAction(): void { $this->moved = false; @@ -107,10 +95,7 @@ protected function callPendingAction() $this->moved = call_user_func_array([$this, $method], $parameters); } - /** - * @return bool - */ - public static function usesSoftDelete() + public static function usesSoftDelete(): bool { static $softDelete; @@ -124,10 +109,7 @@ class_uses_recursive(static::class) return $softDelete; } - /** - * @return bool - */ - protected function actionRaw() + protected function actionRaw(): bool { return true; } @@ -135,7 +117,7 @@ protected function actionRaw() /** * Make a root node. */ - protected function actionRoot() + protected function actionRoot(): bool { // Simplest case that do not affect other nodes. if (! $this->exists) { @@ -152,21 +134,16 @@ protected function actionRoot() /** * Get the lower bound. - * - * @return int */ - protected function getLowerBound() + protected function getLowerBound(): int { return (int) $this->newNestedSetQuery()->max($this->getRgtName()); } /** * Append or prepend a node to the parent. - * - * @param bool $prepend - * @return bool */ - protected function actionAppendOrPrepend(self $parent, $prepend = false) + protected function actionAppendOrPrepend(self $parent, bool $prepend = false): bool { $parent->refreshNode(); @@ -183,11 +160,8 @@ protected function actionAppendOrPrepend(self $parent, $prepend = false) /** * Apply parent model. - * - * @param Model|null $value - * @return $this */ - protected function setParent($value) + protected function setParent(?Model $value): static { $this->setParentId($value ? $value->getKey() : null) ->setRelation('parent', $value); @@ -197,11 +171,8 @@ protected function setParent($value) /** * Insert node before or after another node. - * - * @param bool $after - * @return bool */ - protected function actionBeforeOrAfter(self $node, $after = false) + protected function actionBeforeOrAfter(self $node, bool $after = false): bool { $node->refreshNode(); @@ -211,7 +182,7 @@ protected function actionBeforeOrAfter(self $node, $after = false) /** * Refresh node's crucial attributes. */ - public function refreshNode() + public function refreshNode(): void { if (! $this->exists || static::$actionsPerformed === 0) { return; @@ -225,10 +196,8 @@ public function refreshNode() /** * Relation to the parent. - * - * @return BelongsTo */ - public function parent() + public function parent(): BelongsTo { return $this->belongsTo(get_class($this), $this->getParentIdName()) ->setModel($this); @@ -236,10 +205,8 @@ public function parent() /** * Relation to children. - * - * @return HasMany */ - public function children() + public function children(): HasMany { return $this->hasMany(get_class($this), $this->getParentIdName()) ->setModel($this); @@ -247,20 +214,16 @@ public function children() /** * Get query for descendants of the node. - * - * @return DescendantsRelation */ - public function descendants() + public function descendants(): DescendantsRelation { return new DescendantsRelation($this->newQuery(), $this); } /** * Get query for siblings of the node. - * - * @return QueryBuilder */ - public function siblings() + public function siblings(): QueryBuilder { return $this->newScopedQuery() ->where($this->getKeyName(), '<>', $this->getKey()) @@ -269,10 +232,8 @@ public function siblings() /** * Get the node siblings and the node itself. - * - * @return QueryBuilder */ - public function siblingsAndSelf() + public function siblingsAndSelf(): QueryBuilder { return $this->newScopedQuery() ->where($this->getParentIdName(), '=', $this->getParentId()); @@ -280,21 +241,16 @@ public function siblingsAndSelf() /** * Get query for the node siblings and the node itself. - * - * - * @return EloquentCollection */ - public function getSiblingsAndSelf(array $columns = ['*']) + public function getSiblingsAndSelf(array $columns = ['*']): EloquentCollection { return $this->siblingsAndSelf()->get($columns); } /** * Get query for siblings after the node. - * - * @return QueryBuilder */ - public function nextSiblings() + public function nextSiblings(): QueryBuilder { return $this->nextNodes() ->where($this->getParentIdName(), '=', $this->getParentId()); @@ -302,10 +258,8 @@ public function nextSiblings() /** * Get query for siblings before the node. - * - * @return QueryBuilder */ - public function prevSiblings() + public function prevSiblings(): QueryBuilder { return $this->prevNodes() ->where($this->getParentIdName(), '=', $this->getParentId()); @@ -313,10 +267,8 @@ public function prevSiblings() /** * Get query for nodes after current node. - * - * @return QueryBuilder */ - public function nextNodes() + public function nextNodes(): QueryBuilder { return $this->newScopedQuery() ->where($this->getLftName(), '>', $this->getLft()); @@ -324,10 +276,8 @@ public function nextNodes() /** * Get query for nodes before current node in reversed order. - * - * @return QueryBuilder */ - public function prevNodes() + public function prevNodes(): QueryBuilder { return $this->newScopedQuery() ->where($this->getLftName(), '<', $this->getLft()); @@ -335,20 +285,16 @@ public function prevNodes() /** * Get query ancestors of the node. - * - * @return AncestorsRelation */ - public function ancestors() + public function ancestors(): AncestorsRelation { return new AncestorsRelation($this->newQuery(), $this); } /** * Make this node a root node. - * - * @return $this */ - public function makeRoot() + public function makeRoot(): static { $this->setParent(null)->dirtyBounds(); @@ -357,10 +303,8 @@ public function makeRoot() /** * Save node as root. - * - * @return bool */ - public function saveAsRoot() + public function saveAsRoot(): bool { if ($this->exists && $this->isRoot()) { return $this->save(); @@ -371,53 +315,37 @@ public function saveAsRoot() /** * Append and save a node. - * - * - * @return bool */ - public function appendNode(self $node) + public function appendNode(self $node): bool { return $node->appendToNode($this)->save(); } /** * Prepend and save a node. - * - * - * @return bool */ - public function prependNode(self $node) + public function prependNode(self $node): bool { return $node->prependToNode($this)->save(); } /** * Append a node to the new parent. - * - * - * @return $this */ - public function appendToNode(self $parent) + public function appendToNode(self $parent): static { return $this->appendOrPrependTo($parent); } /** * Prepend a node to the new parent. - * - * - * @return $this */ - public function prependToNode(self $parent) + public function prependToNode(self $parent): static { return $this->appendOrPrependTo($parent, true); } - /** - * @param bool $prepend - * @return self - */ - public function appendOrPrependTo(self $parent, $prepend = false) + public function appendOrPrependTo(self $parent, bool $prepend = false): static { $this->assertNodeExists($parent) ->assertNotDescendant($parent) @@ -430,31 +358,21 @@ public function appendOrPrependTo(self $parent, $prepend = false) /** * Insert self after a node. - * - * - * @return $this */ - public function afterNode(self $node) + public function afterNode(self $node): static { return $this->beforeOrAfterNode($node, true); } /** * Insert self before node. - * - * - * @return $this */ - public function beforeNode(self $node) + public function beforeNode(self $node): static { return $this->beforeOrAfterNode($node); } - /** - * @param bool $after - * @return self - */ - public function beforeOrAfterNode(self $node, $after = false) + public function beforeOrAfterNode(self $node, bool $after = false): static { $this->assertNodeExists($node) ->assertNotDescendant($node) @@ -471,22 +389,16 @@ public function beforeOrAfterNode(self $node, $after = false) /** * Insert self after a node and save. - * - * - * @return bool */ - public function insertAfterNode(self $node) + public function insertAfterNode(self $node): bool { return $this->afterNode($node)->save(); } /** * Insert self before a node and save. - * - * - * @return bool */ - public function insertBeforeNode(self $node) + public function insertBeforeNode(self $node): bool { if (! $this->beforeNode($node)->save()) { return false; @@ -498,10 +410,7 @@ public function insertBeforeNode(self $node) return true; } - /** - * @return $this - */ - public function rawNode($lft, $rgt, $parentId) + public function rawNode(mixed $lft, mixed $rgt, mixed $parentId): static { $this->setLft($lft)->setRgt($rgt)->setParentId($parentId); @@ -510,11 +419,8 @@ public function rawNode($lft, $rgt, $parentId) /** * Move node up given amount of positions. - * - * @param int $amount - * @return bool */ - public function up($amount = 1) + public function up(int $amount = 1): bool { $sibling = $this->prevSiblings() ->defaultOrder('desc') @@ -530,11 +436,8 @@ public function up($amount = 1) /** * Move node down given amount of positions. - * - * @param int $amount - * @return bool */ - public function down($amount = 1) + public function down(int $amount = 1): bool { $sibling = $this->nextSiblings() ->defaultOrder() @@ -550,11 +453,8 @@ public function down($amount = 1) /** * Insert node at specific position. - * - * @param int $position - * @return bool */ - protected function insertAt($position) + protected function insertAt(int $position): bool { static::$actionsPerformed++; @@ -569,11 +469,8 @@ protected function insertAt($position) * Move a node to the new position. * * @since 2.0 - * - * @param int $position - * @return int */ - protected function moveNode($position) + protected function moveNode(int $position): bool { $updated = $this->newNestedSetQuery() ->moveNode($this->getKey(), $position) > 0; @@ -589,11 +486,8 @@ protected function moveNode($position) * Insert new node at specified position. * * @since 2.0 - * - * @param int $position - * @return bool */ - protected function insertNode($position) + protected function insertNode(int $position): bool { $this->newNestedSetQuery()->makeGap($position, 2); @@ -608,7 +502,7 @@ protected function insertNode($position) /** * Update the tree when the node is removed physically. */ - protected function deleteDescendants() + protected function deleteDescendants(): void { $lft = $this->getLft(); $rgt = $this->getRgt(); @@ -634,7 +528,7 @@ protected function deleteDescendants() /** * Restore the descendants. */ - protected function restoreDescendants($deletedAt) + protected function restoreDescendants(mixed $deletedAt): void { $this->descendants() ->where($this->getDeletedAtColumn(), '>=', $deletedAt) @@ -646,7 +540,7 @@ protected function restoreDescendants($deletedAt) * * @since 2.0 */ - public function newEloquentBuilder($query) + public function newEloquentBuilder($query): QueryBuilder { return new QueryBuilder($query); } @@ -655,10 +549,8 @@ public function newEloquentBuilder($query) * Get a new base query that includes deleted nodes. * * @since 1.1 - * - * @return QueryBuilder */ - public function newNestedSetQuery($table = null) + public function newNestedSetQuery(?string $table = null): QueryBuilder { $builder = $this->usesSoftDelete() ? $this->withTrashed() @@ -667,21 +559,16 @@ public function newNestedSetQuery($table = null) return $this->applyNestedSetScope($builder, $table); } - /** - * @param string|null $table - * @return QueryBuilder - */ - public function newScopedQuery($table = null) + public function newScopedQuery(?string $table = null): QueryBuilder { return $this->applyNestedSetScope($this->newQuery(), $table); } /** * @param mixed $query - * @param string|null $table * @return mixed */ - public function applyNestedSetScope($query, $table = null) + public function applyNestedSetScope($query, ?string $table = null) { if (! $scoped = $this->getScopeAttributes()) { return $query; @@ -699,18 +586,12 @@ public function applyNestedSetScope($query, $table = null) return $query; } - /** - * @return array - */ - protected function getScopeAttributes() + protected function getScopeAttributes(): ?array { return null; } - /** - * @return QueryBuilder - */ - public static function scoped(array $attributes) + public static function scoped(array $attributes): QueryBuilder { $instance = new static; @@ -722,7 +603,7 @@ public static function scoped(array $attributes) /** * {@inheritdoc} */ - public function newCollection(array $models = []) + public function newCollection(array $models = []): Collection { return new Collection($models); } @@ -732,7 +613,7 @@ public function newCollection(array $models = []) * * Use `children` key on `$attributes` to create child nodes. */ - public static function create(array $attributes = [], ?self $parent = null) + public static function create(array $attributes = [], ?self $parent = null): static { $children = Arr::pull($attributes, 'children'); @@ -760,10 +641,8 @@ public static function create(array $attributes = [], ?self $parent = null) /** * Get node height (rgt - lft + 1). - * - * @return int */ - public function getNodeHeight() + public function getNodeHeight(): int { if (! $this->exists) { return 2; @@ -787,11 +666,9 @@ public function getDescendantCount() * * Behind the scenes node is appended to found parent node. * - * @param int $value - * * @throws Exception If parent node doesn't exists */ - public function setParentIdAttribute($value) + public function setParentIdAttribute(mixed $value): void { if ($this->getParentId() == $value) { return; @@ -806,78 +683,61 @@ public function setParentIdAttribute($value) /** * Get whether node is root. - * - * @return bool */ - public function isRoot() + public function isRoot(): bool { return is_null($this->getParentId()); } - /** - * @return bool - */ - public function isLeaf() + public function isLeaf(): bool { return $this->getLft() + 1 == $this->getRgt(); } /** * Get the lft key name. - * - * @return string */ - public function getLftName() + public function getLftName(): string { return NestedSet::LFT; } /** * Get the rgt key name. - * - * @return string */ - public function getRgtName() + public function getRgtName(): string { return NestedSet::RGT; } /** * Get the parent id key name. - * - * @return string */ - public function getParentIdName() + public function getParentIdName(): string { return NestedSet::PARENT_ID; } /** * Get the value of the model's lft key. - * - * @return int */ - public function getLft() + public function getLft(): mixed { return $this->getAttributeValue($this->getLftName()); } /** * Get the value of the model's rgt key. - * - * @return int */ - public function getRgt() + public function getRgt(): mixed { return $this->getAttributeValue($this->getRgtName()); } /** * Get the value of the model's parent id key. - * - * @return int */ - public function getParentId() + public function getParentId(): mixed { return $this->getAttributeValue($this->getParentIdName()); } @@ -886,11 +746,8 @@ public function getParentId() * Returns node that is next to current node without constraining to siblings. * * This can be either a next sibling or a next sibling of the parent node. - * - * - * @return self */ - public function getNextNode(array $columns = ['*']) + public function getNextNode(array $columns = ['*']): ?Model { return $this->nextNodes()->defaultOrder()->first($columns); } @@ -899,78 +756,63 @@ public function getNextNode(array $columns = ['*']) * Returns node that is before current node without constraining to siblings. * * This can be either a prev sibling or parent node. - * - * - * @return self */ - public function getPrevNode(array $columns = ['*']) + public function getPrevNode(array $columns = ['*']): ?Model { return $this->prevNodes()->defaultOrder('desc')->first($columns); } - /** - * @return Collection - */ - public function getAncestors(array $columns = ['*']) + public function getAncestors(array $columns = ['*']): Collection { return $this->ancestors()->get($columns); } /** - * @return Collection|self[] + * @return Collection */ - public function getDescendants(array $columns = ['*']) + public function getDescendants(array $columns = ['*']): Collection { return $this->descendants()->get($columns); } /** - * @return Collection|self[] + * @return Collection */ - public function getSiblings(array $columns = ['*']) + public function getSiblings(array $columns = ['*']): Collection { return $this->siblings()->get($columns); } /** - * @return Collection|self[] + * @return Collection */ - public function getNextSiblings(array $columns = ['*']) + public function getNextSiblings(array $columns = ['*']): Collection { return $this->nextSiblings()->get($columns); } /** - * @return Collection|self[] + * @return Collection */ - public function getPrevSiblings(array $columns = ['*']) + public function getPrevSiblings(array $columns = ['*']): Collection { return $this->prevSiblings()->get($columns); } - /** - * @return self - */ - public function getNextSibling(array $columns = ['*']) + public function getNextSibling(array $columns = ['*']): ?Model { return $this->nextSiblings()->defaultOrder()->first($columns); } - /** - * @return self - */ - public function getPrevSibling(array $columns = ['*']) + public function getPrevSibling(array $columns = ['*']): ?Model { return $this->prevSiblings()->defaultOrder('desc')->first($columns); } /** * Get whether a node is a descendant of other node. - * - * - * @return bool */ - public function isDescendantOf(self $other) + public function isDescendantOf(self $other): bool { return $this->getLft() > $other->getLft() && $this->getLft() < $other->getRgt() && @@ -979,11 +821,8 @@ public function isDescendantOf(self $other) /** * Get whether a node is itself or a descendant of other node. - * - * - * @return bool */ - public function isSelfOrDescendantOf(self $other) + public function isSelfOrDescendantOf(self $other): bool { return $this->getLft() >= $other->getLft() && $this->getLft() < $other->getRgt(); @@ -991,62 +830,45 @@ public function isSelfOrDescendantOf(self $other) /** * Get whether the node is immediate children of other node. - * - * - * @return bool */ - public function isChildOf(self $other) + public function isChildOf(self $other): bool { return $this->getParentId() == $other->getKey(); } /** * Get whether the node is a sibling of another node. - * - * - * @return bool */ - public function isSiblingOf(self $other) + public function isSiblingOf(self $other): bool { return $this->getParentId() == $other->getParentId(); } /** * Get whether the node is an ancestor of other node, including immediate parent. - * - * - * @return bool */ - public function isAncestorOf(self $other) + public function isAncestorOf(self $other): bool { return $other->isDescendantOf($this); } /** * Get whether the node is itself or an ancestor of other node, including immediate parent. - * - * - * @return bool */ - public function isSelfOrAncestorOf(self $other) + public function isSelfOrAncestorOf(self $other): bool { return $other->isSelfOrDescendantOf($this); } /** * Get whether the node has moved since last save. - * - * @return bool */ - public function hasMoved() + public function hasMoved(): bool { return $this->moved; } - /** - * @return array - */ - protected function getArrayableRelations() + protected function getArrayableRelations(): array { $result = parent::getArrayableRelations(); @@ -1058,56 +880,39 @@ protected function getArrayableRelations() /** * Get whether user is intended to delete the model from database entirely. - * - * @return bool */ - protected function hardDeleting() + protected function hardDeleting(): bool { return ! $this->usesSoftDelete() || $this->forceDeleting; } - /** - * @return array - */ - public function getBounds() + public function getBounds(): array { return [$this->getLft(), $this->getRgt()]; } - /** - * @return $this - */ - public function setLft($value) + public function setLft(mixed $value): static { $this->attributes[$this->getLftName()] = $value; return $this; } - /** - * @return $this - */ - public function setRgt($value) + public function setRgt(mixed $value): static { $this->attributes[$this->getRgtName()] = $value; return $this; } - /** - * @return $this - */ - public function setParentId($value) + public function setParentId(mixed $value): static { $this->attributes[$this->getParentIdName()] = $value; return $this; } - /** - * @return $this - */ - protected function dirtyBounds() + protected function dirtyBounds(): static { $this->original[$this->getLftName()] = null; $this->original[$this->getRgtName()] = null; @@ -1115,10 +920,7 @@ protected function dirtyBounds() return $this; } - /** - * @return $this - */ - protected function assertNotDescendant(self $node) + protected function assertNotDescendant(self $node): static { if ($node == $this || $node->isDescendantOf($this)) { throw new LogicException('Node must not be a descendant.'); @@ -1127,10 +929,7 @@ protected function assertNotDescendant(self $node) return $this; } - /** - * @return $this - */ - protected function assertNodeExists(self $node) + protected function assertNodeExists(self $node): static { if (! $node->getLft() || ! $node->getRgt()) { throw new LogicException('Node must exists.'); @@ -1139,7 +938,7 @@ protected function assertNodeExists(self $node) return $this; } - protected function assertSameScope(self $node) + protected function assertSameScope(self $node): void { if (! $scoped = $this->getScopeAttributes()) { return; @@ -1167,10 +966,7 @@ protected function isSameScope(self $node): bool return true; } - /** - * @return Model - */ - public function replicate(?array $except = null) + public function replicate(?array $except = null): static { $defaults = [ $this->getParentIdName(), diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 93521a0..48cea75 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -22,12 +22,8 @@ class QueryBuilder extends Builder * Get node's `lft` and `rgt` values. * * @since 2.0 - * - * @param mixed $id - * @param bool $required - * @return array */ - public function getNodeData($id, $required = false) + public function getNodeData(mixed $id, bool $required = false): array { $query = $this->toBase(); @@ -47,22 +43,16 @@ public function getNodeData($id, $required = false) * Get plain node data. * * @since 2.0 - * - * @param mixed $id - * @param bool $required - * @return array */ - public function getPlainNodeData($id, $required = false) + public function getPlainNodeData(mixed $id, bool $required = false): array { return array_values($this->getNodeData($id, $required)); } /** * Scope limits query to select just root node. - * - * @return $this */ - public function whereIsRoot() + public function whereIsRoot(): static { $this->query->whereNull($this->model->getParentIdName()); @@ -73,13 +63,8 @@ public function whereIsRoot() * Limit results to ancestors of specified node. * * @since 2.0 - * - * @param mixed $id - * @param bool $andSelf - * @param string $boolean - * @return $this */ - public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') + public function whereAncestorOf(mixed $id, bool $andSelf = false, string $boolean = 'and'): static { $keyName = $this->model->getTable().'.'.$this->model->getKeyName(); $model = null; @@ -124,19 +109,12 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') return $this; } - /** - * @param bool $andSelf - * @return $this - */ - public function orWhereAncestorOf($id, $andSelf = false) + public function orWhereAncestorOf(mixed $id, bool $andSelf = false): static { return $this->whereAncestorOf($id, $andSelf, 'or'); } - /** - * @return QueryBuilder - */ - public function whereAncestorOrSelf($id) + public function whereAncestorOrSelf(mixed $id): static { return $this->whereAncestorOf($id, true); } @@ -145,19 +123,13 @@ public function whereAncestorOrSelf($id) * Get ancestors of specified node. * * @since 2.0 - * - * @param mixed $id - * @return Collection */ - public function ancestorsOf($id, array $columns = ['*']) + public function ancestorsOf(mixed $id, array $columns = ['*']): Collection { return $this->whereAncestorOf($id)->get($columns); } - /** - * @return Collection - */ - public function ancestorsAndSelf($id, array $columns = ['*']) + public function ancestorsAndSelf(mixed $id, array $columns = ['*']): Collection { return $this->whereAncestorOf($id, true)->get($columns); } @@ -166,14 +138,8 @@ public function ancestorsAndSelf($id, array $columns = ['*']) * Add node selection statement between specified range. * * @since 2.0 - * - * @param array $values - * @param string $boolean - * @param bool $not - * @param Query $query - * @return $this */ - public function whereNodeBetween($values, $boolean = 'and', $not = false, $query = null) + public function whereNodeBetween(array $values, string $boolean = 'and', bool $not = false, ?Query $query = null): static { ($query ?? $this->query)->whereBetween($this->model->getTable().'.'.$this->model->getLftName(), $values, $boolean, $not); @@ -184,11 +150,8 @@ public function whereNodeBetween($values, $boolean = 'and', $not = false, $query * Add node selection statement between specified range joined with `or` operator. * * @since 2.0 - * - * @param array $values - * @return $this */ - public function orWhereNodeBetween($values) + public function orWhereNodeBetween(array $values): static { return $this->whereNodeBetween($values, 'or'); } @@ -197,16 +160,10 @@ public function orWhereNodeBetween($values) * Add constraint statement to descendants of specified node. * * @since 2.0 - * - * @param mixed $id - * @param string $boolean - * @param bool $not - * @param bool $andSelf - * @return $this */ - public function whereDescendantOf($id, $boolean = 'and', $not = false, - $andSelf = false - ) { + public function whereDescendantOf(mixed $id, string $boolean = 'and', bool $not = false, + bool $andSelf = false + ): static { $this->query->whereNested(function (Query $inner) use ($id, $andSelf, $not) { if (NestedSet::isNode($id)) { $id->applyNestedSetScope($inner); @@ -229,39 +186,22 @@ public function whereDescendantOf($id, $boolean = 'and', $not = false, return $this; } - /** - * @param mixed $id - * @return QueryBuilder - */ - public function whereNotDescendantOf($id) + public function whereNotDescendantOf(mixed $id): static { return $this->whereDescendantOf($id, 'and', true); } - /** - * @param mixed $id - * @return QueryBuilder - */ - public function orWhereDescendantOf($id) + public function orWhereDescendantOf(mixed $id): static { return $this->whereDescendantOf($id, 'or'); } - /** - * @param mixed $id - * @return QueryBuilder - */ - public function orWhereNotDescendantOf($id) + public function orWhereNotDescendantOf(mixed $id): static { return $this->whereDescendantOf($id, 'or', true); } - /** - * @param string $boolean - * @param bool $not - * @return $this - */ - public function whereDescendantOrSelf($id, $boolean = 'and', $not = false) + public function whereDescendantOrSelf(mixed $id, string $boolean = 'and', bool $not = false): static { return $this->whereDescendantOf($id, $boolean, $not, true); } @@ -270,12 +210,8 @@ public function whereDescendantOrSelf($id, $boolean = 'and', $not = false) * Get descendants of specified node. * * @since 2.0 - * - * @param mixed $id - * @param bool $andSelf - * @return Collection */ - public function descendantsOf($id, array $columns = ['*'], $andSelf = false) + public function descendantsOf(mixed $id, array $columns = ['*'], bool $andSelf = false): Collection { try { return $this->whereDescendantOf($id, 'and', false, $andSelf)->get($columns); @@ -284,18 +220,12 @@ public function descendantsOf($id, array $columns = ['*'], $andSelf = false) } } - /** - * @return Collection - */ - public function descendantsAndSelf($id, array $columns = ['*']) + public function descendantsAndSelf(mixed $id, array $columns = ['*']): Collection { return $this->descendantsOf($id, $columns, true); } - /** - * @return $this - */ - protected function whereIsBeforeOrAfter($id, $operator, $boolean) + protected function whereIsBeforeOrAfter(mixed $id, string $operator, string $boolean): static { if (NestedSet::isNode($id)) { $value = '?'; @@ -325,12 +255,8 @@ protected function whereIsBeforeOrAfter($id, $operator, $boolean) * Constraint nodes to those that are after specified node. * * @since 2.0 - * - * @param mixed $id - * @param string $boolean - * @return $this */ - public function whereIsAfter($id, $boolean = 'and') + public function whereIsAfter(mixed $id, string $boolean = 'and'): static { return $this->whereIsBeforeOrAfter($id, '>', $boolean); } @@ -339,41 +265,28 @@ public function whereIsAfter($id, $boolean = 'and') * Constraint nodes to those that are before specified node. * * @since 2.0 - * - * @param mixed $id - * @param string $boolean - * @return $this */ - public function whereIsBefore($id, $boolean = 'and') + public function whereIsBefore(mixed $id, string $boolean = 'and'): static { return $this->whereIsBeforeOrAfter($id, '<', $boolean); } - /** - * @return $this - */ - public function whereIsLeaf() + public function whereIsLeaf(): static { [$lft, $rgt] = $this->wrappedColumns(); return $this->whereRaw("$lft = $rgt - 1"); } - /** - * @return Collection - */ - public function leaves(array $columns = ['*']) + public function leaves(array $columns = ['*']): Collection { return $this->whereIsLeaf()->get($columns); } /** * Include depth level into the result. - * - * @param string $as - * @return $this */ - public function withDepth($as = 'depth') + public function withDepth(string $as = 'depth'): static { if ($this->query->columns === null) { $this->query->columns = ['*']; @@ -402,10 +315,8 @@ public function withDepth($as = 'depth') * Get wrapped `lft` and `rgt` column names. * * @since 2.0 - * - * @return array */ - protected function wrappedColumns() + protected function wrappedColumns(): array { $grammar = $this->query->getGrammar(); @@ -419,10 +330,8 @@ protected function wrappedColumns() * Get a wrapped table name. * * @since 2.0 - * - * @return string */ - protected function wrappedTable() + protected function wrappedTable(): string { return $this->query->getGrammar()->wrapTable($this->getQuery()->from); } @@ -431,20 +340,16 @@ protected function wrappedTable() * Wrap model's key name. * * @since 2.0 - * - * @return string */ - protected function wrappedKey() + protected function wrappedKey(): string { return $this->query->getGrammar()->wrap($this->model->getKeyName()); } /** * Exclude root node from the result. - * - * @return $this */ - public function withoutRoot() + public function withoutRoot(): static { $this->query->whereNotNull($this->model->getParentIdName()); @@ -456,10 +361,8 @@ public function withoutRoot() * * @since 2.0 * @deprecated since v4.1 - * - * @return $this */ - public function hasParent() + public function hasParent(): static { $this->query->whereNotNull($this->model->getParentIdName()); @@ -471,10 +374,8 @@ public function hasParent() * * @since 2.0 * @deprecated since v4.1 - * - * @return $this */ - public function hasChildren() + public function hasChildren(): static { [$lft, $rgt] = $this->wrappedColumns(); @@ -485,11 +386,8 @@ public function hasChildren() /** * Order by node position. - * - * @param string $dir - * @return $this */ - public function defaultOrder($dir = 'asc') + public function defaultOrder(string $dir = 'asc'): static { $this->query->orders = null; @@ -500,22 +398,16 @@ public function defaultOrder($dir = 'asc') /** * Order by reversed node position. - * - * @return $this */ - public function reversed() + public function reversed(): static { return $this->defaultOrder('desc'); } /** * Move a node to the new position. - * - * @param mixed $key - * @param int $position - * @return int */ - public function moveNode($key, $position) + public function moveNode(mixed $key, int $position): int { [$lft, $rgt] = $this->model->newNestedSetQuery() ->getPlainNodeData($key, true); @@ -561,12 +453,8 @@ public function moveNode($key, $position) * Make or remove gap in the tree. Negative height will remove gap. * * @since 2.0 - * - * @param int $cut - * @param int $height - * @return int */ - public function makeGap($cut, $height) + public function makeGap(int $cut, int $height): int { $params = compact('cut', 'height'); @@ -582,10 +470,8 @@ public function makeGap($cut, $height) * Get patch for columns. * * @since 2.0 - * - * @return array */ - protected function patch(array $params) + protected function patch(array $params): array { $grammar = $this->query->getGrammar(); @@ -602,11 +488,8 @@ protected function patch(array $params) * Get patch for single column. * * @since 2.0 - * - * @param string $col - * @return string */ - protected function columnPatch($col, array $params) + protected function columnPatch(string $col, array $params): Expression { extract($params); @@ -639,10 +522,8 @@ protected function columnPatch($col, array $params) * Get statistics of errors of the tree. * * @since 2.0 - * - * @return array */ - public function countErrors() + public function countErrors(): array { $checks = []; @@ -669,10 +550,7 @@ public function countErrors() return (array) $query->first(); } - /** - * @return BaseQueryBuilder - */ - protected function getOdnessQuery() + protected function getOdnessQuery(): BaseQueryBuilder { return $this->model ->newNestedSetQuery() @@ -685,10 +563,7 @@ protected function getOdnessQuery() }); } - /** - * @return BaseQueryBuilder - */ - protected function getDuplicatesQuery() + protected function getDuplicatesQuery(): BaseQueryBuilder { $table = $this->wrappedTable(); $keyName = $this->wrappedKey(); @@ -716,10 +591,7 @@ protected function getDuplicatesQuery() return $this->model->applyNestedSetScope($query, $secondAlias); } - /** - * @return BaseQueryBuilder - */ - protected function getWrongParentQuery() + protected function getWrongParentQuery(): BaseQueryBuilder { $table = $this->wrappedTable(); $keyName = $this->wrappedKey(); @@ -757,10 +629,7 @@ protected function getWrongParentQuery() return $query; } - /** - * @return $this - */ - protected function getMissingParentQuery() + protected function getMissingParentQuery(): BaseQueryBuilder { return $this->model ->newNestedSetQuery() @@ -793,10 +662,8 @@ protected function getMissingParentQuery() * Get the number of total errors of the tree. * * @since 2.0 - * - * @return int */ - public function getTotalErrors() + public function getTotalErrors(): int { return array_sum($this->countErrors()); } @@ -805,10 +672,8 @@ public function getTotalErrors() * Get whether the tree is broken. * * @since 2.0 - * - * @return bool */ - public function isBroken() + public function isBroken(): bool { return $this->getTotalErrors() > 0; } @@ -821,7 +686,7 @@ public function isBroken() * @param null|NodeTrait|Model $root * @return int The number of changed nodes */ - public function fixTree($root = null) + public function fixTree($root = null): int { $columns = [ $this->model->getKeyName(), @@ -845,18 +710,16 @@ public function fixTree($root = null) /** * @param NodeTrait|Model $root - * @return int */ - public function fixSubtree($root) + public function fixSubtree($root): int { return $this->fixTree($root); } /** * @param NodeTrait|Model|null $parent - * @return int */ - protected function fixNodes(array &$dictionary, $parent = null) + protected function fixNodes(array &$dictionary, $parent = null): int { $parentId = $parent ? $parent->getKey() : null; $cut = $parent ? $parent->getLft() + 1 : 1; @@ -889,14 +752,11 @@ protected function fixNodes(array &$dictionary, $parent = null) } /** - * @param int $cut - * @return int - * * @internal param int $fixed */ protected static function reorderNodes( - array &$dictionary, array &$updated, $parentId = null, $cut = 1 - ) { + array &$dictionary, array &$updated, mixed $parentId = null, int $cut = 1 + ): int { if (! isset($dictionary[$parentId])) { return $cut; } @@ -927,9 +787,8 @@ protected static function reorderNodes( * @param bool $delete Whether to delete nodes that exists but not in the data * array * @param null $root - * @return int */ - public function rebuildTree(array $data, $delete = false, $root = null) + public function rebuildTree(array $data, bool $delete = false, $root = null): int { if ($this->model->usesSoftDelete()) { $this->withTrashed(); @@ -972,23 +831,16 @@ public function rebuildTree(array $data, $delete = false, $root = null) return $this->fixNodes($dictionary, $root); } - /** - * @param bool $delete - * @return int - */ - public function rebuildSubtree($root, array $data, $delete = false) + public function rebuildSubtree($root, array $data, bool $delete = false): int { return $this->rebuildTree($data, $delete, $root); } - /** - * @param mixed $parentId - */ protected function buildRebuildDictionary(array &$dictionary, array $data, array &$existing, - $parentId = null - ) { + mixed $parentId = null + ): void { $keyName = $this->model->getKeyName(); foreach ($data as $itemData) { @@ -1026,22 +878,15 @@ protected function buildRebuildDictionary(array &$dictionary, } } - /** - * @param string|null $table - * @return $this - */ - public function applyNestedSetScope($table = null) + public function applyNestedSetScope(?string $table = null): static { return $this->model->applyNestedSetScope($this, $table); } /** * Get the root node. - * - * - * @return self */ - public function root(array $columns = ['*']) + public function root(array $columns = ['*']): ?Model { return $this->whereIsRoot()->first($columns); } From d38773c07bf576f7ec98f9f1c708eb335826fe2a Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Mon, 1 Jun 2026 14:35:36 +0100 Subject: [PATCH 6/7] Add Larastan static analysis Analyse src/ at level 5 with a baseline capturing the pre-existing trait-analysis findings (PHPStan's known limitation when a trait is analysed outside its host model). New code is held to level 5. Add a composer `analyse` script and a CI static-analysis job that also enforces Pint formatting. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/run-tests.yml | 30 +++ composer.json | 6 +- phpstan-baseline.neon | 343 ++++++++++++++++++++++++++++++++ phpstan.neon | 9 + 4 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c099af3..2e0933a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -97,6 +97,36 @@ jobs: DB_USERNAME: postgres DB_PASSWORD: password + static-analysis: + runs-on: ubuntu-latest + + timeout-minutes: 15 + + env: + COMPOSER_NO_INTERACTION: 1 + + name: Static Analysis + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: composer update -o --quiet --prefer-dist + + - name: Run Pint + run: composer lint + + - name: Run PHPStan + run: composer analyse + coverage: runs-on: ubuntu-latest diff --git a/composer.json b/composer.json index 512be35..e5b7e6e 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,8 @@ "require-dev": { "phpunit/phpunit": "^11.0", "laravel/pint": "^1.29", - "orchestra/testbench": "^10.0|^11.0" + "orchestra/testbench": "^10.0|^11.0", + "larastan/larastan": "^3.0" }, "minimum-stability": "stable", "prefer-stable": true, @@ -59,6 +60,9 @@ ], "lint": [ "@php ./vendor/bin/pint --test" + ], + "analyse": [ + "@php ./vendor/bin/phpstan analyse" ] } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..7ed81b4 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,343 @@ +parameters: + ignoreErrors: + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:isAncestorOf\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/AncestorsRelation.php + + - + message: '#^Call to method getKeyName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/AncestorsRelation.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:newScopedQuery\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/BaseRelation.php + + - + message: '#^Call to method getLftName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/BaseRelation.php + + - + message: '#^Call to method getRgtName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/BaseRelation.php + + - + message: '#^Call to method newQuery\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/BaseRelation.php + + - + message: '#^PHPDoc type Illuminate\\Database\\Eloquent\\Model\|Lunar\\Nestedset\\NodeTrait of property Lunar\\Nestedset\\BaseRelation\:\:\$parent is not covariant with PHPDoc type Illuminate\\Database\\Eloquent\\Model of overridden property Illuminate\\Database\\Eloquent\\Relations\\Relation\\:\:\$parent\.$#' + identifier: property.phpDocType + count: 1 + path: src/BaseRelation.php + + - + message: '#^Parameter \#1 \$query of method Lunar\\Nestedset\\BaseRelation\:\:addEagerConstraint\(\) expects Lunar\\Nestedset\\QueryBuilder, Illuminate\\Database\\Eloquent\\Builder\ given\.$#' + identifier: argument.type + count: 1 + path: src/BaseRelation.php + + - + message: '#^Property Lunar\\Nestedset\\BaseRelation\:\:\$parent has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: property.trait + count: 1 + path: src/BaseRelation.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:getParentIdName\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Collection.php + + - + message: '#^Call to method getKey\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/Collection.php + + - + message: '#^Call to method getLft\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/Collection.php + + - + message: '#^Call to method getParentId\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 3 + path: src/Collection.php + + - + message: '#^Call to method setRelation\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 3 + path: src/Collection.php + + - + message: '#^Empty array passed to foreach\.$#' + identifier: foreach.emptyArray + count: 2 + path: src/Collection.php + + - + message: '#^PHPDoc tag @var for variable \$child has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: varTag.trait + count: 1 + path: src/Collection.php + + - + message: '#^PHPDoc tag @var for variable \$node has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: varTag.trait + count: 3 + path: src/Collection.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 3 + path: src/Collection.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:isDescendantOf\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/DescendantsRelation.php + + - + message: '#^Trait Lunar\\Nestedset\\NodeTrait is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/NodeTrait.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:rawNode\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to an undefined method Lunar\\Nestedset\\QueryBuilder\:\:withTrashed\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method applyNestedSetScope\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 5 + path: src/QueryBuilder.php + + - + message: '#^Call to method freshTimestamp\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method fromDateTime\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method getAttributes\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method getKey\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method getKeyName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 8 + path: src/QueryBuilder.php + + - + message: '#^Call to method getLft\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method getLftName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 9 + path: src/QueryBuilder.php + + - + message: '#^Call to method getParentId\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method getParentIdName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 7 + path: src/QueryBuilder.php + + - + message: '#^Call to method getRgt\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method getRgtName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 7 + path: src/QueryBuilder.php + + - + message: '#^Call to method getTable\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 6 + path: src/QueryBuilder.php + + - + message: '#^Call to method isDirty\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method newCollection\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method newInstance\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method newNestedSetQuery\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 8 + path: src/QueryBuilder.php + + - + message: '#^Call to method newQuery\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method newScopedQuery\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 3 + path: src/QueryBuilder.php + + - + message: '#^Call to method rawNode\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method usesSoftDelete\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 3 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:ancestorsAndSelf\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:ancestorsOf\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:descendantsOf\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\<\(int\|string\), Illuminate\\Database\\Eloquent\\Model\>\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:descendantsOf\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:leaves\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:whereIsLeaf\(\) should return static\(Lunar\\Nestedset\\QueryBuilder\) but returns Lunar\\Nestedset\\QueryBuilder\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^PHPDoc tag @var for variable \$model has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: varTag.trait + count: 3 + path: src/QueryBuilder.php + + - + message: '#^PHPDoc type Illuminate\\Database\\Eloquent\\Model\|Lunar\\Nestedset\\NodeTrait of property Lunar\\Nestedset\\QueryBuilder\:\:\$model is not covariant with PHPDoc type Illuminate\\Database\\Eloquent\\Model of overridden property Illuminate\\Database\\Eloquent\\Builder\\:\:\$model\.$#' + identifier: property.phpDocType + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Parameter \$parent of method Lunar\\Nestedset\\QueryBuilder\:\:fixNodes\(\) has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: parameter.trait + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Parameter \$root of method Lunar\\Nestedset\\QueryBuilder\:\:fixSubtree\(\) has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: parameter.trait + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Parameter \$root of method Lunar\\Nestedset\\QueryBuilder\:\:fixTree\(\) has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: parameter.trait + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Property Lunar\\Nestedset\\QueryBuilder\:\:\$model has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: property.trait + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Ternary operator condition is always false\.$#' + identifier: ternary.alwaysFalse + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Variable \$model in PHPDoc tag @var does not exist\.$#' + identifier: varTag.variableNotFound + count: 1 + path: src/QueryBuilder.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..b73e9a3 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +includes: + - vendor/larastan/larastan/extension.neon + - phpstan-baseline.neon + +parameters: + level: 5 + + paths: + - src From 6c9b5c3ef1b1590b4a4aecbe42c3fa5166b92d36 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Mon, 1 Jun 2026 14:36:31 +0100 Subject: [PATCH 7/7] Add CHANGELOG and CONTRIBUTING, document dev workflow Add a Contributing section to the README covering the test/lint/analyse scripts and multi-engine testing, plus CONTRIBUTING.md and CHANGELOG.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 16 +++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f4fb3fc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to `lunarphp/nestedset` are documented here. The format is +based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### Fixed + +- `NestedSet::isNode()` always returned `false` for genuine nodes because it + inspected object properties instead of used traits. It now uses + `class_uses_recursive()`. + +### Added + +- Native PHP type declarations across the library (PHP 8.3+). +- Laravel Pint for code style. +- Larastan static analysis (level 5) with a baseline for pre-existing findings. +- Test suite now runs against SQLite, MySQL and PostgreSQL in CI, plus a code + coverage job. +- `CONTRIBUTING.md` and this changelog. + +### Changed + +- Test suite migrated from a hand-rolled Capsule bootstrap to Orchestra + Testbench, exercising the package through a real Laravel application + (service provider registration and Blueprint macros are now covered). + +## Fork + +`lunarphp/nestedset` is a fork of +[`kalnoy/nestedset`](https://github.com/lazychaser/laravel-nestedset) by +Alexander Kalnoy, maintained for Lunar and kept current with modern Laravel +(12 and 13) and PHP 8.3+. For the history prior to the fork, see the upstream +project. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d340414 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +Thanks for contributing to `lunarphp/nestedset`. + +## Getting started + +``` +git clone https://github.com/lunarphp/nestedset +cd nestedset +composer install +``` + +## Before opening a pull request + +Run the full quality suite locally and make sure it passes: + +``` +composer lint # code style (Laravel Pint) +composer analyse # static analysis (Larastan) +composer test # test suite +``` + +`composer format` will fix most style issues automatically. + +## Tests + +The suite runs against SQLite by default. To run it against MySQL or +PostgreSQL, set the connection via environment variables — for example using +Docker: + +``` +docker run -d --rm -e POSTGRES_DB=nestedset -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=password -p 5432:5432 postgres:16 + +DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=nestedset \ + DB_USERNAME=postgres DB_PASSWORD=password composer test +``` + +The CI workflow exercises PHP 8.3/8.4, Laravel 12/13 and all three database +engines, so changes that depend on engine-specific behaviour should be covered +by a test. + +## Static analysis + +New code is analysed at PHPStan level 5. Pre-existing findings are captured in +`phpstan-baseline.neon`; please do not add to the baseline — fix new findings +instead. + +## Reporting bugs + +Open an issue with a minimal reproduction (model definition, schema and the +sequence of calls that triggers the problem). diff --git a/README.md b/README.md index afb71d9..bc1ba98 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ by Alexander Kalnoy, kept current for modern Laravel. - [Helper methods](#helper-methods) - [Checking consistency](#checking-consistency) - [Scoping](#scoping) +- [Contributing](#contributing) - [License](#license) ## What are nested sets? @@ -659,6 +660,21 @@ After [setting up your model](#the-model), fix the tree to populate `_lft` and MyModel::fixTree(); ``` +## Contributing + +Contributions are welcome. The test suite runs against SQLite, MySQL and +PostgreSQL: + +``` +composer test # run the test suite +composer lint # check code style (Laravel Pint) +composer format # fix code style +composer analyse # run static analysis (Larastan) +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for details, and +[CHANGELOG.md](CHANGELOG.md) for release notes. + ## License Released under the [MIT license](https://opensource.org/licenses/MIT).