diff --git a/phpstan.neon b/phpstan.neon index 7656bd8..b899433 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,4 +9,46 @@ parameters: stubFiles: ignoreErrors: - identifier: missingType.generics - - '#Interface must be located in "Contract" or "Contracts" namespace#' \ No newline at end of file + - '#Interface must be located in "Contract" or "Contracts" namespace#' + - '#Dynamic call to static method Kalnoy\\Nestedset\\QueryBuilder<.*>::select\(\).#' + - '#Dynamic call to static method Kalnoy\\Nestedset\\QueryBuilder<.*>::from\(\).#' + - '#Dynamic call to static method Kalnoy\\Nestedset\\QueryBuilder<.*>::whereRaw\(\).#' + - '#Dynamic call to static method Kalnoy\\Nestedset\\QueryBuilder<.*>::whereNested\(\).#' + # bunch of false positives from Eloquent + # ---------------------------------------------------------------------------------------- + - '#Parameter .* \$models .* of method .*::initRelation\(\) should be contravariant with parameter \$models .* of method .*::initRelation\(\)#' + - '#Parameter .* \$models .* of method .*::addEagerConstraints\(\) should be contravariant with parameter \$models .* of method .*::addEagerConstraints\(\)#' + - '#Parameter .* \$models .* of method .*::match\(\) should be contravariant with parameter \$models .* of method .*::match\(\)#' + + # - '#Method .*::ancestorsOf\(\) should return Illuminate\\Database\\Eloquent\\Collection<.*, .* Illuminate\\Database\\Eloquent\\Model> but returns Illuminate\\Database\\Eloquent\\Collection.#' + # - '#Method .*::ancestorsAndSelf\(\) should return Illuminate\\Database\\Eloquent\\Collection<.*, .* Illuminate\\Database\\Eloquent\\Model> but returns Illuminate\\Database\\Eloquent\\Collection.#' + # - '#Method .*::descendantsOf\(\) should return Illuminate\\Database\\Eloquent\\Collection<.*, .* Illuminate\\Database\\Eloquent\\Model> but returns Illuminate\\Database\\Eloquent\\Collection.#' + # - '#Method .*::descendantsAndSelf\(\) should return Illuminate\\Database\\Eloquent\\Collection<.*, .* Illuminate\\Database\\Eloquent\\Model> but returns Illuminate\\Database\\Eloquent\\Collection.#' + # - '#Parameter #1 $query of method Kalnoy\Nestedset\BaseRelation::addEagerConstraint() expects + # Kalnoy\Nestedset\QueryBuilder, Illuminate\Database\Eloquent\Builder given.#' + # - '#Parameter \#1 \$model \(Illuminate\\Database\\Eloquent\\Model&Kalnoy\\Nestedset\\Node\) of method Kalnoy\\Nestedset\\AncestorsRelation::matches\(\) should be contravariant with parameter \$model \(Illuminate\\Database\\Eloquent\\Model\) of method Kalnoy\\Nestedset\\BaseRelation::matches\(\)#' + # - '#Parameter \#2 \$related \(Kalnoy\\Nestedset\\Node\) of method Kalnoy\\Nestedset\\AncestorsRelation::matches\(\) should be contravariant with parameter \$related \(mixed\) of method Kalnoy\\Nestedset\\BaseRelation::matches\(\)#' + # - '#Parameter \#1 \$models \(array\) of method Kalnoy\\Nestedset\\BaseRelation::initRelation\(\) should be contravariant with parameter \$models \(array\) of method Illuminate\\Database\\Eloquent\\Relations\\Relation::initRelation\(\)#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::from\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::limit\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::offset\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::take\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::truncate\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::insert\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::select\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::orderBy\(\)#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::where(Not)?(Null|In|Between|Exists|Column|Year|Month|Day)?\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::delete\(\)#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::without\(\)#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::with\(\)#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::count\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::update\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::inRandomOrder\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::groupBy\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::latest\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::first\(\).#' + # - '#Dynamic call to static method (Illuminate\\Database\\Query\\Builder|Illuminate\\Database\\Eloquent\\(Builder|Relations\\.*))(<.*>)?::skip\(\).#' + # - '#Call to an undefined method Illuminate\\Database\\Eloquent\\.*::with(Only)?\(\)#' + # - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder|Illuminate\\Database\\Eloquent\\Relations\\Relation::whereNotNull\(\).#' + # - '#Call to protected method asDateTime\(\) of class Illuminate\\Database\\Eloquent\\Model.#' \ No newline at end of file diff --git a/src/AncestorsRelation.php b/src/AncestorsRelation.php index 68dbeaf..f5d212b 100644 --- a/src/AncestorsRelation.php +++ b/src/AncestorsRelation.php @@ -5,7 +5,7 @@ use Illuminate\Database\Eloquent\Model; /** - * @template T + * @disregard P1037 */ class AncestorsRelation extends BaseRelation { @@ -25,12 +25,12 @@ public function addConstraints() } /** - * @param Model $model - * @param $related + * @param Model&Node $model + * @param Node $related * * @return bool */ - protected function matches(Model $model, $related) + protected function matches(Model $model, $related): bool { return $related->isAncestorOf($model); } diff --git a/src/BaseRelation.php b/src/BaseRelation.php index 141a44f..ff77a8c 100644 --- a/src/BaseRelation.php +++ b/src/BaseRelation.php @@ -8,6 +8,14 @@ use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Query\Builder; +/** + * @template Tmodelkey + * @template Tmodel of Model&Node + * @extends Relation + * @property Tmodel $related + * @property Tmodel $parent + * @method Tmodel getParent() + */ abstract class BaseRelation extends Relation { /** @@ -43,12 +51,12 @@ public function __construct(QueryBuilder $builder, Model $model) } /** - * @param Model $model - * @param $related + * @param Model&Node $model + * @param Node $related * * @return bool */ - abstract protected function matches(Model $model, $related); + abstract protected function matches(Model&Node $model, Node $related): bool; /** * @param QueryBuilder $query @@ -71,14 +79,14 @@ abstract protected function relationExistenceCondition(string $hash, string $tab /** * @param EloquentBuilder $query * @param EloquentBuilder $parent - * @param string[] $columns + * @param mixed $columns * - * @return mixed + * @return EloquentBuilder */ public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilder $parent, $columns = ['*'] ) { - $query = $this->getParent()->replicate()->newScopedQuery()->select($columns); + $query = $this->getParent()->replicate()->newScopedQuery()->select($columns); $table = $query->getModel()->getTable(); @@ -100,10 +108,10 @@ public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilde /** * Initialize the relation on a set of models. * - * @param array $models + * @param array $models * @param string $relation * - * @return array + * @return array */ public function initRelation(array $models, $relation) { @@ -135,7 +143,7 @@ public function getResults() /** * Set the constraints for an eager load of the relation. * - * @param array $models + * @param array $models * * @return void */ @@ -160,11 +168,11 @@ public function addEagerConstraints(array $models) /** * Match the eagerly loaded results to their parents. * - * @param array $models + * @param array $models * @param EloquentCollection $results * @param string $relation * - * @return array + * @return array */ public function match(array $models, EloquentCollection $results, $relation) { @@ -178,15 +186,17 @@ public function match(array $models, EloquentCollection $results, $relation) } /** - * @param Model $model + * @param Tmodel $model * @param EloquentCollection $results * * @return Collection */ protected function matchForModel(Model $model, EloquentCollection $results) { + /** @var Collection */ $result = $this->related->newCollection(); + /** @var Tmodel $related */ foreach ($results as $related) { if ($this->matches($model, $related)) { $result->push($related); diff --git a/src/Collection.php b/src/Collection.php index cfc563f..e38664b 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -2,10 +2,16 @@ namespace Kalnoy\Nestedset; -use Illuminate\Database\Eloquent\Collection as BaseCollection; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; -final class Collection extends BaseCollection +/** + * @template TKey of array-key + * @template TModel of Model&Node + * + * @extends EloquentCollection + */ +final class Collection extends EloquentCollection { /** * Fill `parent` and `children` relationships for every node in the collection. @@ -20,7 +26,9 @@ public function linkNodes() return $this; } - $groupedNodes = $this->groupBy($this->first()->getParentIdName()); + /** @var Node */ + $first = $this->first(); + $groupedNodes = $this->groupBy($first->getParentIdName()); /** @var Node&Model $node */ foreach ($this->items as $node) { @@ -28,14 +36,14 @@ public function linkNodes() $node->setRelation('parent', null); } + /** @var array */ $children = $groupedNodes->get($node->getKey(), []); - /** @var Model&Node $child */ foreach ($children as $child) { $child->setRelation('parent', $node); } - $node->setRelation('children', BaseCollection::make($children)); + $node->setRelation('children', EloquentCollection::make($children)); } return $this; @@ -77,7 +85,7 @@ public function toTree($root = false) /** * @param mixed $root * - * @return int|string|null + * @return int|string */ protected function getRootNodeId($root = false) { @@ -101,6 +109,10 @@ protected function getRootNodeId($root = false) } } + if ($root === null || $root === false) { + throw new NestedSetException('root is null or false.'); + } + return $root; } @@ -110,17 +122,20 @@ protected function getRootNodeId($root = false) * * @param bool $root * - * @return static + * @return Collection */ - public function toFlatTree($root = false) + public function toFlatTree($root = false): Collection { - $result = new static(); + $result = new Collection(); if ($this->isEmpty()) { return $result; } - $groupedNodes = $this->groupBy($this->first()->getParentIdName()); + /** @var Node */ + $first = $this->first(); + /** @var Collection */ + $groupedNodes = $this->groupBy($first->getParentIdName()); return $result->flattenTree($groupedNodes, $this->getRootNodeId($root)); } @@ -128,14 +143,16 @@ public function toFlatTree($root = false) /** * Flatten a tree into a non recursive array. * - * @param Collection $groupedNodes - * @param mixed $parentId + * @param Collection $groupedNodes + * @param int|string $parentId * - * @return $this + * @return Collection */ - protected function flattenTree(self $groupedNodes, $parentId) + protected function flattenTree(Collection $groupedNodes, $parentId): Collection { - foreach ($groupedNodes->get($parentId, []) as $node) { + /** @var array */ + $nodes = $groupedNodes->get($parentId, []); + foreach ($nodes as $node) { $this->push($node); $this->flattenTree($groupedNodes, $node->getKey()); diff --git a/src/DescendantsRelation.php b/src/DescendantsRelation.php index 185e73d..89039cf 100644 --- a/src/DescendantsRelation.php +++ b/src/DescendantsRelation.php @@ -4,6 +4,9 @@ use Illuminate\Database\Eloquent\Model; +/** + * @disregard P1037 + */ class DescendantsRelation extends BaseRelation { /** @@ -31,12 +34,12 @@ protected function addEagerConstraint($query, $model) } /** - * @param Model $model - * @param $related + * @param Model&Node $model + * @param Node $related * - * @return mixed + * @return bool */ - protected function matches(Model $model, $related) + protected function matches(Model $model, $related): bool { return $related->isDescendantOf($model); } diff --git a/src/NestedSetException.php b/src/NestedSetException.php new file mode 100644 index 0000000..7df7aeb --- /dev/null +++ b/src/NestedSetException.php @@ -0,0 +1,15 @@ + */ public function siblings(): QueryBuilder; /** * Get the node siblings and the node itself. * - * @return QueryBuilder + * @return QueryBuilder */ public function siblingsAndSelf(): QueryBuilder; @@ -68,28 +75,28 @@ public function getSiblingsAndSelf(array $columns = ['*']): EloquentCollection; /** * Get query for siblings after the node. * - * @return QueryBuilder + * @return QueryBuilder */ public function nextSiblings(): QueryBuilder; /** * Get query for siblings before the node. * - * @return QueryBuilder + * @return QueryBuilder */ public function prevSiblings(): QueryBuilder; /** * Get query for nodes after current node. * - * @return QueryBuilder + * @return QueryBuilder */ public function nextNodes(): QueryBuilder; /** * Get query for nodes before current node in reversed order. * - * @return QueryBuilder + * @return QueryBuilder */ public function prevNodes(): QueryBuilder; @@ -115,13 +122,13 @@ public function makeRoot(): Node; public function saveAsRoot(): bool; /** - * @param int $lft - * @param int $rgt - * @param int|string|null $parentId + * @param int $lft + * @param int $rgt + * @param Tmodelkey $parentId * * @return $this */ - public function rawNode(int $lft, int $rgt, int|string|null $parentId): Node; + public function rawNode(int $lft, int $rgt, mixed $parentId): Node; /** * Move node up given amount of positions. @@ -151,14 +158,14 @@ public function newEloquentBuilder(QueryBuilder $query): QueryBuilder; * * @since 1.1 * - * @return QueryBuilder + * @return QueryBuilder */ - public function newNestedSetQuery(?QueryBuilder $table = null): QueryBuilder; + public function newNestedSetQuery(QueryBuilder|string|null $table = null): QueryBuilder; /** * @param ?string $table * - * @return QueryBuilder + * @return QueryBuilder */ public function newScopedQuery($table = null); @@ -173,16 +180,14 @@ public function applyNestedSetScope($query, $table = null); /** * @param string[] $attributes * - * @return QueryBuilder + * @return QueryBuilder */ public static function scoped(array $attributes): QueryBuilder; /** - * @template Tmodel of \Illuminate\Database\Eloquent\Model + * @param array $models * - * @param array $models - * - * @return Collection + * @return Collection> */ public function newCollection(array $models = []): Collection; @@ -201,11 +206,11 @@ public function getDescendantCount(): int; * * Behind the scenes node is appended to found parent node. * - * @param int|string|null $value + * @param Tmodelkey|null $value * * @throws \Exception If parent node doesn't exists */ - public function setParentIdAttribute(int|string|null $value): void; + public function setParentIdAttribute(mixed $value): void; /** * Get whether node is root. @@ -242,9 +247,9 @@ public function getRgt(): int|null; /** * Get the value of the model's parent id key. * - * @return int|string|null + * @return Tmodelkey|null */ - public function getParentId(): int|string|null; + public function getParentId(): mixed; /** * Returns node that is next to current node without constraining to siblings. @@ -325,28 +330,90 @@ public function getBounds(); /** * @param $value * - * @return $this + * @return Node&Tmodel */ public function setLft(int $value): Node; /** * @param $value * - * @return $this + * @return Node&Tmodel */ public function setRgt(int $value): Node; /** - * @param $value + * @param Tmodelkey|null $id * - * @return $this + * @return Node&Tmodel */ - public function setParentId(int|string|null $value): Node; + public function setParentId(mixed $id): Node; /** * @param string[]|null $except * - * @return $this + * @return Node */ public function replicate(?array $except = null): Node; + + + /** + * Append and save a node. + * + * @param Node&Tmodel $node + * + * @return bool + */ + public function appendNode(Node $node): bool; + + /** + * Append a node to the new parent. + * + * @param Node&Tmodel $parent + * + * @return Node&Tmodel + */ + public function appendToNode(Node $parent): Node; + + /** + * Prepend a node to the new parent. + * + * @param Node $parent + * + * @return Node&Tmodel + */ + public function prependToNode(Node $parent): Node; + + /** + * Get whether the node is an ancestor of other node, including immediate parent. + * + * @param Node $other + * + * @return bool + */ + public function isAncestorOf(Node $other): bool; + + /** + * Get whether a node is a descendant of other node. + * + * @param Node $other + * + * @return bool + */ + public function isDescendantOf(Node $other): bool; + + /** + * Get whether a node is itself or a descendant of other node. + * + * @param Node $other + * + * @return bool + */ + public function isSelfOrDescendantOf(Node $other); + + /** + * Create a new Query + * + * @return QueryBuilder + */ + public function newQuery(); } diff --git a/src/NodeTrait.php b/src/NodeTrait.php index f23c088..818566d 100644 --- a/src/NodeTrait.php +++ b/src/NodeTrait.php @@ -9,6 +9,10 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Arr; +/** + * @template Tmodelkey + * @template Tmodel extends Model + */ trait NodeTrait { /** @@ -63,7 +67,7 @@ public static function bootNodeTrait(): void * * @return $this */ - protected function setNodeAction($action): self + protected function setNodeAction($action): Node { $this->pending = func_get_args(); @@ -146,12 +150,12 @@ protected function getLowerBound(): int /** * Append or prepend a node to the parent. * - * @param self $parent + * @param Node $parent * @param bool $prepend * * @return bool */ - protected function actionAppendOrPrepend(self $parent, $prepend = false) + protected function actionAppendOrPrepend(Node $parent, $prepend = false) { $parent->refreshNode(); @@ -184,12 +188,12 @@ protected function setParent($value) /** * Insert node before or after another node. * - * @param self $node + * @param Node $node * @param bool $after * * @return bool */ - protected function actionBeforeOrAfter(self $node, $after = false) + protected function actionBeforeOrAfter(Node $node, $after = false) { $node->refreshNode(); @@ -199,7 +203,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; @@ -361,23 +365,24 @@ public function saveAsRoot(): bool /** * Append and save a node. * - * @param self $node + * @param Node&Tmodel $node * * @return bool */ - public function appendNode(self $node): bool + public function appendNode(Node $node): bool { + /** @disregard P1013 */ return $node->appendToNode($this)->save(); } /** * Prepend and save a node. * - * @param self $node + * @param Node $node * * @return bool */ - public function prependNode(self $node): bool + public function prependNode(Node $node): bool { return $node->prependToNode($this)->save(); } @@ -385,11 +390,11 @@ public function prependNode(self $node): bool /** * Append a node to the new parent. * - * @param self $parent + * @param Node&Tmodel $parent * - * @return $this + * @return Node&Tmodel */ - public function appendToNode(self $parent): self + public function appendToNode(Node $parent): Node { return $this->appendOrPrependTo($parent); } @@ -397,22 +402,22 @@ public function appendToNode(self $parent): self /** * Prepend a node to the new parent. * - * @param self $parent + * @param Node $parent * * @return $this */ - public function prependToNode(self $parent): self + public function prependToNode(Node $parent): Node { return $this->appendOrPrependTo($parent, true); } /** - * @param self $parent + * @param Node $parent * @param bool $prepend * - * @return self + * @return Node */ - public function appendOrPrependTo(self $parent, $prepend = false) + public function appendOrPrependTo(Node $parent, bool $prepend = false) { $this->assertNodeExists($parent) ->assertNotDescendant($parent) @@ -426,11 +431,11 @@ public function appendOrPrependTo(self $parent, $prepend = false) /** * Insert self after a node. * - * @param self $node + * @param Node $node * * @return $this */ - public function afterNode(self $node) + public function afterNode(Node $node) { return $this->beforeOrAfterNode($node, true); } @@ -438,22 +443,22 @@ public function afterNode(self $node) /** * Insert self before node. * - * @param self $node + * @param Node $node * * @return $this */ - public function beforeNode(self $node) + public function beforeNode(Node $node) { return $this->beforeOrAfterNode($node); } /** - * @param self $node + * @param Node&Tmodel $node * @param bool $after * - * @return self + * @return Node */ - public function beforeOrAfterNode(self $node, $after = false) + public function beforeOrAfterNode(Node $node, bool $after = false) { $this->assertNodeExists($node) ->assertNotDescendant($node) @@ -471,11 +476,11 @@ public function beforeOrAfterNode(self $node, $after = false) /** * Insert self after a node and save. * - * @param self $node + * @param Node $node * * @return bool */ - public function insertAfterNode(self $node) + public function insertAfterNode(Node $node) { return $this->afterNode($node)->save(); } @@ -483,11 +488,11 @@ public function insertAfterNode(self $node) /** * Insert self before a node and save. * - * @param self $node + * @param Node $node * * @return bool */ - public function insertBeforeNode(self $node) + public function insertBeforeNode(Node $node) { if (!$this->beforeNode($node)->save()) { return false; @@ -502,11 +507,11 @@ public function insertBeforeNode(self $node) /** * @param int $lft * @param int $rgt - * @param int|string|null $parentId + * @param Tmodelkey|null $parentId * - * @return $this + * @return Node */ - public function rawNode(int $lft, int $rgt, int|string|null $parentId): self + public function rawNode(int $lft, int $rgt, mixed $parentId): Node { $this->setLft($lft)->setRgt($rgt)->setParentId($parentId); @@ -582,7 +587,7 @@ protected function insertAt($position) * * @return int */ - protected function moveNode($position) + protected function moveNode(int $position) { $updated = $this->newNestedSetQuery() ->moveNode($this->getKey(), $position) > 0; @@ -603,7 +608,7 @@ protected function moveNode($position) * * @return bool */ - protected function insertNode($position) + protected function insertNode(int $position) { $this->newNestedSetQuery()->makeGap($position, 2); @@ -772,9 +777,9 @@ public function newCollection(array $models = []): Collection * * Use `children` key on `$attributes` to create child nodes. * - * @param self $parent + * @param Node $parent */ - public static function create(array $attributes = [], ?self $parent = null) + public static function create(array $attributes = [], ?Node $parent = null) { $children = Arr::pull($attributes, 'children'); @@ -825,18 +830,20 @@ public function getDescendantCount(): int * * Behind the scenes node is appended to found parent node. * - * @param int|string|null $value + * @param Tmodelkey|null $value * * @throws \Exception If parent node doesn't exists */ - public function setParentIdAttribute(int|string|null $value): void + public function setParentIdAttribute(mixed $value): void { if ($this->getParentId() == $value) { return; } if ($value) { - $this->appendToNode($this->newScopedQuery()->findOrFail($value)); + /** @var Node&Tmodel */ + $node = $this->newScopedQuery()->findOrFail($value); + $this->appendToNode($node); } else { $this->makeRoot(); } @@ -898,9 +905,9 @@ public function getRgt(): int|null /** * Get the value of the model's parent id key. * - * @return int|string|null + * @return Tmodelkey|null */ - public function getParentId(): int|string|null + public function getParentId(): mixed { return $this->getAttributeValue($this->getParentIdName()); } @@ -912,9 +919,9 @@ public function getParentId(): int|string|null * * @param string[] $columns * - * @return self + * @return Node */ - public function getNextNode(array $columns = ['*']): self + public function getNextNode(array $columns = ['*']): Node { return $this->nextNodes()->defaultOrder()->first($columns); } @@ -926,9 +933,9 @@ public function getNextNode(array $columns = ['*']): self * * @param string[] $columns * - * @return self + * @return Node */ - public function getPrevNode(array $columns = ['*']): self + public function getPrevNode(array $columns = ['*']): Node { return $this->prevNodes()->defaultOrder('desc')->first($columns); } @@ -946,7 +953,7 @@ public function getAncestors(array $columns = ['*']) /** * @param string[] $columns * - * @return Collection|self[] + * @return Collection|Node[] */ public function getDescendants(array $columns = ['*']) { @@ -956,7 +963,7 @@ public function getDescendants(array $columns = ['*']) /** * @param string[] $columns * - * @return Collection|self[] + * @return Collection|Node[] */ public function getSiblings(array $columns = ['*']) { @@ -966,7 +973,7 @@ public function getSiblings(array $columns = ['*']) /** * @param string[] $columns * - * @return Collection|self[] + * @return Collection|Node[] */ public function getNextSiblings(array $columns = ['*']) { @@ -976,7 +983,7 @@ public function getNextSiblings(array $columns = ['*']) /** * @param string[] $columns * - * @return Collection|self[] + * @return Collection|Node[] */ public function getPrevSiblings(array $columns = ['*']) { @@ -986,7 +993,7 @@ public function getPrevSiblings(array $columns = ['*']) /** * @param string[] $columns * - * @return self + * @return Node */ public function getNextSibling(array $columns = ['*']) { @@ -996,7 +1003,7 @@ public function getNextSibling(array $columns = ['*']) /** * @param string[] $columns * - * @return self + * @return Node */ public function getPrevSibling(array $columns = ['*']) { @@ -1006,11 +1013,11 @@ public function getPrevSibling(array $columns = ['*']) /** * Get whether a node is a descendant of other node. * - * @param self $other + * @param Node $other * * @return bool */ - public function isDescendantOf(self $other) + public function isDescendantOf(Node $other): bool { return $this->getLft() > $other->getLft() && $this->getLft() < $other->getRgt(); @@ -1019,11 +1026,11 @@ public function isDescendantOf(self $other) /** * Get whether a node is itself or a descendant of other node. * - * @param self $other + * @param Node $other * * @return bool */ - public function isSelfOrDescendantOf(self $other) + public function isSelfOrDescendantOf(Node $other) { return $this->getLft() >= $other->getLft() && $this->getLft() < $other->getRgt(); @@ -1032,11 +1039,11 @@ public function isSelfOrDescendantOf(self $other) /** * Get whether the node is immediate children of other node. * - * @param self $other + * @param Node&Tmodel $other * * @return bool */ - public function isChildOf(self $other) + public function isChildOf(Node $other) { return $this->getParentId() == $other->getKey(); } @@ -1044,11 +1051,11 @@ public function isChildOf(self $other) /** * Get whether the node is a sibling of another node. * - * @param self $other + * @param Node $other * * @return bool */ - public function isSiblingOf(self $other) + public function isSiblingOf(Node $other) { return $this->getParentId() == $other->getParentId(); } @@ -1056,11 +1063,11 @@ public function isSiblingOf(self $other) /** * Get whether the node is an ancestor of other node, including immediate parent. * - * @param self $other + * @param Node $other * * @return bool */ - public function isAncestorOf(self $other) + public function isAncestorOf(Node $other): bool { return $other->isDescendantOf($this); } @@ -1068,11 +1075,11 @@ public function isAncestorOf(self $other) /** * Get whether the node is itself or an ancestor of other node, including immediate parent. * - * @param self $other + * @param Node $other * * @return bool */ - public function isSelfOrAncestorOf(self $other) + public function isSelfOrAncestorOf(Node $other) { return $other->isSelfOrDescendantOf($this); } @@ -1121,9 +1128,9 @@ public function getBounds() /** * @param $value * - * @return self $this + * @return Node $this */ - public function setLft(int $value): self + public function setLft(int $value): Node { $this->attributes[$this->getLftName()] = $value; @@ -1133,9 +1140,9 @@ public function setLft(int $value): self /** * @param $value * - * @return self $this + * @return Node $this */ - public function setRgt(int $value): self + public function setRgt(int $value): Node { $this->attributes[$this->getRgtName()] = $value; @@ -1143,11 +1150,11 @@ public function setRgt(int $value): self } /** - * @param int|string|null $value + * @param Tmodelkey|null $value * - * @return self $this + * @return Node&Tmodel */ - public function setParentId(int|string|null $value): self + public function setParentId(mixed $value): Node { $this->attributes[$this->getParentIdName()] = $value; @@ -1155,7 +1162,7 @@ public function setParentId(int|string|null $value): self } /** - * @return $this + * @return Node */ protected function dirtyBounds() { @@ -1166,11 +1173,11 @@ protected function dirtyBounds() } /** - * @param self $node + * @param Node $node * - * @return $this + * @return Node */ - protected function assertNotDescendant(self $node) + protected function assertNotDescendant(Node $node) { if ($node == $this || $node->isDescendantOf($this)) { throw new \LogicException('Node must not be a descendant.'); @@ -1180,11 +1187,11 @@ protected function assertNotDescendant(self $node) } /** - * @param self $node + * @param Node $node * - * @return $this + * @return Node&Tmodel */ - protected function assertNodeExists(self $node) + protected function assertNodeExists(Node $node) { if (!$node->getLft() || !$node->getRgt()) { throw new \LogicException('Node must exists.'); @@ -1194,9 +1201,9 @@ protected function assertNodeExists(self $node) } /** - * @param self $node + * @param Node&Tmodel $node */ - protected function assertSameScope(self $node) + protected function assertSameScope(Node $node): void { if (!$scoped = $this->getScopeAttributes()) { return; @@ -1214,7 +1221,7 @@ protected function assertSameScope(self $node) * * @return \Illuminate\Database\Eloquent\Model */ - public function replicate(?array $except = null): self + public function replicate(?array $except = null): Node { $defaults = [ $this->getParentIdName(), diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 934bb01..76ec9c0 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -3,6 +3,7 @@ namespace Kalnoy\Nestedset; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Query\Builder as BaseQueryBuilder; @@ -10,10 +11,17 @@ use Illuminate\Database\Query\Expression; use Illuminate\Support\Arr; +/** + * @template Tmodelkey + * @template Tmodel of \Illuminate\Database\Eloquent\Model + * + * @phpstan-type NodeModel Node&Tmodel + * @extends Builder + */ class QueryBuilder extends Builder { /** - * @var Node&Model + * @var NodeModel */ protected $model; @@ -22,12 +30,12 @@ class QueryBuilder extends Builder * * @since 2.0 * - * @param mixed $id + * @param Tmodelkey $id * @param bool $required * - * @return array + * @return array */ - public function getNodeData($id, $required = false) + public function getNodeData(mixed $id, $required = false) { $query = $this->toBase(); @@ -36,7 +44,7 @@ public function getNodeData($id, $required = false) $data = $query->first([$this->model->getLftName(), $this->model->getRgtName(), ]); - if (!$data && $required) { + if ($data === null && $required) { throw new ModelNotFoundException(); } @@ -48,12 +56,12 @@ public function getNodeData($id, $required = false) * * @since 2.0 * - * @param mixed $id + * @param Tmodelkey $id * @param bool $required * - * @return array + * @return array */ - public function getPlainNodeData($id, $required = false) + public function getPlainNodeData(mixed $id, $required = false) { return array_values($this->getNodeData($id, $required)); } @@ -61,9 +69,9 @@ public function getPlainNodeData($id, $required = false) /** * Scope limits query to select just root node. * - * @return $this + * @return QueryBuilder */ - public function whereIsRoot() + public function whereIsRoot(): QueryBuilder { $this->query->whereNull($this->model->getParentIdName()); @@ -75,13 +83,13 @@ public function whereIsRoot() * * @since 2.0 * - * @param mixed $id + * @param Tmodelkey $id * @param bool $andSelf * @param string $boolean * - * @return $this + * @return QueryBuilder */ - public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') + public function whereAncestorOf(mixed $id, bool $andSelf = false, string $boolean = 'and') { $keyName = $this->model->getTable() . '.' . $this->model->getKeyName(); @@ -120,22 +128,22 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') } /** - * @param $id + * @param Tmodelkey $id * @param bool $andSelf * - * @return $this + * @return QueryBuilder */ - public function orWhereAncestorOf($id, $andSelf = false) + public function orWhereAncestorOf(mixed $id, $andSelf = false): QueryBuilder { return $this->whereAncestorOf($id, $andSelf, 'or'); } /** - * @param $id + * @param Tmodelkey $id * - * @return QueryBuilder + * @return QueryBuilder */ - public function whereAncestorOrSelf($id) + public function whereAncestorOrSelf(mixed $id): QueryBuilder { return $this->whereAncestorOf($id, true); } @@ -145,23 +153,23 @@ public function whereAncestorOrSelf($id) * * @since 2.0 * - * @param mixed $id - * @param array $columns + * @param Tmodelkey $id + * @param string[] $columns * - * @return \Kalnoy\Nestedset\Collection + * @return EloquentCollection */ - public function ancestorsOf($id, array $columns = ['*']) + public function ancestorsOf(mixed $id, array $columns = ['*']) { return $this->whereAncestorOf($id)->get($columns); } /** - * @param $id - * @param array $columns + * @param Tmodelkey $id + * @param string[] $columns * - * @return \Kalnoy\Nestedset\Collection + * @return EloquentCollection */ - public function ancestorsAndSelf($id, array $columns = ['*']) + public function ancestorsAndSelf(mixed $id, array $columns = ['*']) { return $this->whereAncestorOf($id, true)->get($columns); } @@ -171,13 +179,13 @@ public function ancestorsAndSelf($id, array $columns = ['*']) * * @since 2.0 * - * @param array $values + * @param int[] $values * @param string $boolean * @param bool $not * - * @return $this + * @return QueryBuilder */ - public function whereNodeBetween($values, $boolean = 'and', $not = false) + public function whereNodeBetween(array $values, $boolean = 'and', $not = false) { $this->query->whereBetween($this->model->getTable() . '.' . $this->model->getLftName(), $values, $boolean, $not); @@ -189,11 +197,11 @@ public function whereNodeBetween($values, $boolean = 'and', $not = false) * * @since 2.0 * - * @param array $values + * @param int[] $values * - * @return $this + * @return QueryBuilder */ - public function orWhereNodeBetween($values) + public function orWhereNodeBetween(array $values) { return $this->whereNodeBetween($values, 'or'); } @@ -203,14 +211,14 @@ public function orWhereNodeBetween($values) * * @since 2.0 * - * @param mixed $id + * @param Tmodelkey $id * @param string $boolean * @param bool $not * @param bool $andSelf * - * @return $this + * @return QueryBuilder */ - public function whereDescendantOf($id, $boolean = 'and', $not = false, + public function whereDescendantOf(mixed $id, $boolean = 'and', $not = false, $andSelf = false ) { if (NestedSet::isNode($id)) { @@ -229,43 +237,43 @@ public function whereDescendantOf($id, $boolean = 'and', $not = false, } /** - * @param mixed $id + * @param Tmodelkey $id * * @return QueryBuilder */ - public function whereNotDescendantOf($id) + public function whereNotDescendantOf(mixed $id) { return $this->whereDescendantOf($id, 'and', true); } /** - * @param mixed $id + * @param Tmodelkey $id * * @return QueryBuilder */ - public function orWhereDescendantOf($id) + public function orWhereDescendantOf(mixed $id) { return $this->whereDescendantOf($id, 'or'); } /** - * @param mixed $id + * @param Tmodelkey $id * * @return QueryBuilder */ - public function orWhereNotDescendantOf($id) + public function orWhereNotDescendantOf(mixed $id) { return $this->whereDescendantOf($id, 'or', true); } /** - * @param $id + * @param Tmodelkey $id * @param string $boolean * @param bool $not * - * @return $this + * @return QueryBuilder */ - public function whereDescendantOrSelf($id, $boolean = 'and', $not = false) + public function whereDescendantOrSelf(mixed $id, string $boolean = 'and', bool $not = false) { return $this->whereDescendantOf($id, $boolean, $not, true); } @@ -275,13 +283,13 @@ public function whereDescendantOrSelf($id, $boolean = 'and', $not = false) * * @since 2.0 * - * @param mixed $id - * @param array $columns + * @param Tmodelkey $id + * @param string[] $columns * @param bool $andSelf * - * @return Collection + * @return EloquentCollection */ - public function descendantsOf($id, array $columns = ['*'], $andSelf = false) + public function descendantsOf(mixed $id, array $columns = ['*'], bool $andSelf = false) { try { return $this->whereDescendantOf($id, 'and', false, $andSelf)->get($columns); @@ -291,10 +299,10 @@ public function descendantsOf($id, array $columns = ['*'], $andSelf = false) } /** - * @param $id - * @param array $columns + * @param Tmodelkey $id + * @param string[] $columns * - * @return Collection + * @return EloquentCollection */ public function descendantsAndSelf($id, array $columns = ['*']) { @@ -302,13 +310,13 @@ public function descendantsAndSelf($id, array $columns = ['*']) } /** - * @param $id - * @param $operator - * @param $boolean + * @param Tmodelkey $id + * @param string $operator + * @param string $boolean * - * @return $this + * @return QueryBuilder */ - protected function whereIsBeforeOrAfter($id, $operator, $boolean) + protected function whereIsBeforeOrAfter(mixed $id, string $operator, string $boolean) { if (NestedSet::isNode($id)) { $value = '?'; @@ -339,10 +347,10 @@ protected function whereIsBeforeOrAfter($id, $operator, $boolean) * * @since 2.0 * - * @param mixed $id + * @param Tmodelkey $id * @param string $boolean * - * @return $this + * @return QueryBuilder */ public function whereIsAfter($id, $boolean = 'and') { @@ -354,10 +362,10 @@ public function whereIsAfter($id, $boolean = 'and') * * @since 2.0 * - * @param mixed $id + * @param Tmodelkey $id * @param string $boolean * - * @return $this + * @return QueryBuilder */ public function whereIsBefore($id, $boolean = 'and') { @@ -365,19 +373,19 @@ public function whereIsBefore($id, $boolean = 'and') } /** - * @return $this + * @return QueryBuilder */ public function whereIsLeaf() { list($lft, $rgt) = $this->wrappedColumns(); - return $this->whereRaw("$lft = $rgt - 1"); + return $this->whereRaw("$lft = $rgt - 1"); /** @phpstan-ignore-line */ } /** - * @param array $columns + * @param string[] $columns * - * @return Collection + * @return EloquentCollection */ public function leaves(array $columns = ['*']) { @@ -389,7 +397,7 @@ public function leaves(array $columns = ['*']) * * @param string $as * - * @return $this + * @return QueryBuilder */ public function withDepth($as = 'depth') { @@ -421,9 +429,9 @@ public function withDepth($as = 'depth') * * @since 2.0 * - * @return array + * @return string[] */ - protected function wrappedColumns() + protected function wrappedColumns(): array { $grammar = $this->query->getGrammar(); @@ -440,7 +448,7 @@ protected function wrappedColumns() * * @return string */ - protected function wrappedTable() + protected function wrappedTable(): string { return $this->query->getGrammar()->wrapTable($this->getQuery()->from); } @@ -452,7 +460,7 @@ protected function wrappedTable() * * @return string */ - protected function wrappedKey() + protected function wrappedKey(): string { return $this->query->getGrammar()->wrap($this->model->getKeyName()); } @@ -460,9 +468,9 @@ protected function wrappedKey() /** * Exclude root node from the result. * - * @return $this + * @return QueryBuilder */ - public function withoutRoot() + public function withoutRoot(): QueryBuilder { $this->query->whereNotNull($this->model->getParentIdName()); @@ -475,9 +483,9 @@ public function withoutRoot() * @since 2.0 * @deprecated since v4.1 * - * @return $this + * @return QueryBuilder */ - public function hasParent() + public function hasParent(): QueryBuilder { $this->query->whereNotNull($this->model->getParentIdName()); @@ -490,9 +498,9 @@ public function hasParent() * @since 2.0 * @deprecated since v4.1 * - * @return $this + * @return QueryBuilder */ - public function hasChildren() + public function hasChildren(): QueryBuilder { list($lft, $rgt) = $this->wrappedColumns(); @@ -506,9 +514,9 @@ public function hasChildren() * * @param string $dir * - * @return $this + * @return QueryBuilder */ - public function defaultOrder($dir = 'asc') + public function defaultOrder($dir = 'asc'): QueryBuilder { $this->query->orders = []; @@ -520,9 +528,9 @@ public function defaultOrder($dir = 'asc') /** * Order by reversed node position. * - * @return $this + * @return QueryBuilder */ - public function reversed() + public function reversed(): QueryBuilder { return $this->defaultOrder('desc'); } @@ -604,11 +612,11 @@ public function makeGap($cut, $height) * * @since 2.0 * - * @param array $params + * @param array{height:int,cut?:int,distance?:int,lft?:int,rgt?:int,to?:int,from?:int} $params * - * @return array + * @return array */ - protected function patch(array $params) + protected function patch(array $params): array { $grammar = $this->query->getGrammar(); @@ -627,15 +635,14 @@ protected function patch(array $params) * @since 2.0 * * @param string $col - * @param array $params + * @param array{height:int,cut?:int,distance?:int,lft?:int,rgt?:int,to?:int,from?:int} $params * - * @return string + * @return Expression */ - protected function columnPatch($col, array $params) + protected function columnPatch(string $col, array $params): Expression { extract($params); - /** @var int $height */ if ($height > 0) { $height = '+' . $height; } @@ -644,11 +651,11 @@ protected function columnPatch($col, array $params) return new Expression("case when {$col} >= {$cut} then {$col}{$height} else {$col} end"); } - /** @var int $distance */ - /** @var int $lft */ - /** @var int $rgt */ - /** @var int $from */ - /** @var int $to */ + if (!isset($distance) || !isset($lft) || !isset($rgt) || !isset($to) || !isset($from)) + { + throw new NestedSetException('Incorrect Parameters'); + } + if ($distance > 0) { $distance = '+' . $distance; } @@ -784,7 +791,7 @@ protected function getWrongParentQuery() } /** - * @return $this + * @return QueryBuilder */ protected function getMissingParentQuery() { @@ -844,7 +851,7 @@ public function isBroken() * * Nodes with invalid parent are saved as roots. * - * @param NodeTrait|Model|null $root + * @param (Model&Node)|null $root * * @return int The number of changed nodes */ @@ -871,7 +878,7 @@ public function fixTree($root = null) } /** - * @param NodeTrait|Model $root + * @param Model&Node $root * * @return int */ @@ -882,7 +889,7 @@ public function fixSubtree($root) /** * @param array $dictionary - * @param NodeTrait|Model|null $parent + * @param (Model&Node)|null $parent * * @return int */ @@ -935,7 +942,7 @@ protected static function reorderNodes( return $cut; } - /** @var Model|NodeTrait $model */ + /** @var Model&Node $model */ foreach ($dictionary[$parentId] as $model) { $lft = $cut; @@ -983,7 +990,7 @@ public function rebuildTree(array $data, $delete = false, $root = null) $this->buildRebuildDictionary($dictionary, $data, $existing, $parentId); - /** @var Model|NodeTrait $model */ + /** @var Model&Node $model */ if (!empty($existing)) { if ($delete && !$this->model->usesSoftDelete()) { $this->model @@ -1034,7 +1041,7 @@ protected function buildRebuildDictionary(array &$dictionary, $keyName = $this->model->getKeyName(); foreach ($data as $itemData) { - /** @var NodeTrait|Model $model */ + /** @var Model&Node $model */ if (!isset($itemData[$keyName])) { $model = $this->model->newInstance($this->model->getAttributes()); @@ -1071,7 +1078,7 @@ protected function buildRebuildDictionary(array &$dictionary, /** * @param string|null $table * - * @return $this + * @return QueryBuilder */ public function applyNestedSetScope($table = null) { diff --git a/tests/models/Category.php b/tests/models/Category.php index 241482e..c031f3d 100644 --- a/tests/models/Category.php +++ b/tests/models/Category.php @@ -1,11 +1,14 @@