diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 46f272b..ad1e490 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -19,9 +19,9 @@ jobs: if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 - with: - access_token: ${{ github.token }} + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 + with: + access_token: ${{ github.token }} php_syntax_errors: name: 1️⃣ PHP - Syntax errors diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..4acd070 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,15 @@ +parameters: + ignoreErrors: + + - + message: '#^Call to an undefined method Kalnoy\\Nestedset\\Contracts\\NodeQueryBuilder\\:\:whereIn\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Dynamic call to static method Kalnoy\\Nestedset\\QueryBuilder\\:\:whereRaw\(\)\.$#' + identifier: staticMethod.dynamicCall + count: 1 + path: src/QueryBuilder.php + diff --git a/phpstan.neon b/phpstan.neon index 1e5e209..a0e5e0d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,18 +1,17 @@ includes: - vendor/larastan/larastan/extension.neon - vendor/lychee-org/phpstan-lychee/phpstan.neon + - phpstan-baseline.neon parameters: - level: 3 + level: 6 paths: - src + - tests/models/ excludePaths: stubFiles: ignoreErrors: - # - identifier: missingType.generics - - '#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\(\).#' - - '#Dynamic call to static method Kalnoy\\Nestedset\\QueryBuilder<.*>::whereIn\(\).#' + - '#.*covariant.*#' + - '#.*contravariant.*#' + - + message: '#^Call to an undefined method Kalnoy\\Nestedset\\Contracts\\Node<.*>::assert.*\(\)\.$#' \ No newline at end of file diff --git a/src/AncestorsRelation.php b/src/AncestorsRelation.php index 2fabefe..1d2b5f9 100644 --- a/src/AncestorsRelation.php +++ b/src/AncestorsRelation.php @@ -3,17 +3,17 @@ namespace Kalnoy\Nestedset; use Illuminate\Database\Eloquent\Model; +use Kalnoy\Nestedset\Contracts\Node; +use Kalnoy\Nestedset\Contracts\NodeQueryBuilder; /** * @template Tmodel of Model * - * @phpstan-type NodeModel Node&Tmodel - * - * @disregard P1037 + * @phpstan-type NodeModel \Kalnoy\Nestedset\Contracts\Node&Tmodel * * @extends BaseRelation */ -class AncestorsRelation extends BaseRelation +final class AncestorsRelation extends BaseRelation { /** * Set the base constraints on the relation query. @@ -36,14 +36,14 @@ public function addConstraints() * * @return bool */ - protected function matches(Model $model, $related): bool + protected function matches(Node $model, Node $related): bool { return $related->isAncestorOf($model); } /** - * @param QueryBuilder $query - * @param NodeModel $model + * @param NodeQueryBuilder $query + * @param NodeModel $model * * @return void */ @@ -57,6 +57,7 @@ protected function addEagerConstraint($query, $model) */ protected function relationExistenceCondition(string $hash, string $table, string $lft, string $rgt): string { + /** @disregard P1013 */ $key = $this->getBaseQuery()->getGrammar()->wrap($this->parent->getKeyName()); return "{$table}.{$rgt} between {$hash}.{$lft} and {$hash}.{$rgt} and $table.$key <> $hash.$key"; diff --git a/src/BaseRelation.php b/src/BaseRelation.php index e263b4d..a59a708 100644 --- a/src/BaseRelation.php +++ b/src/BaseRelation.php @@ -7,23 +7,24 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Query\Builder; +use Kalnoy\Nestedset\Contracts\NestedSetCollection; +use Kalnoy\Nestedset\Contracts\Node; +use Kalnoy\Nestedset\Contracts\NodeQueryBuilder; /** * @template Tmodel of Model * - * @phpstan-type NodeModel Node&Tmodel + * @phpstan-type NodeModel Node&Tmodel * - * @extends Relation> + * @extends Relation> * * @property NodeModel $related * @property NodeModel $parent - * - * @method NodeModel getParent() */ abstract class BaseRelation extends Relation { /** - * @var QueryBuilder + * @var NodeQueryBuilder */ protected $query; @@ -42,8 +43,8 @@ abstract class BaseRelation extends Relation /** * AncestorsRelation constructor. * - * @param QueryBuilder $builder - * @param NodeModel $model + * @param QueryBuilder $builder + * @param NodeModel $model */ public function __construct(QueryBuilder $builder, Model $model) { @@ -60,11 +61,11 @@ public function __construct(QueryBuilder $builder, Model $model) * * @return bool */ - abstract protected function matches(Model&Node $model, Node $related): bool; + abstract protected function matches(Node $model, Node $related): bool; /** - * @param QueryBuilder $query - * @param NodeModel $model + * @param NodeQueryBuilder $query + * @param NodeModel $model * * @return void */ @@ -90,6 +91,7 @@ abstract protected function relationExistenceCondition(string $hash, string $tab public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilder $parentQuery, $columns = ['*'], ) { + /** @disregard P1006 */ $query = $this->getParent()->replicate()->newScopedQuery()->select($columns); $table = $query->getModel()->getTable(); @@ -106,7 +108,7 @@ public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilde $grammar->wrap($this->parent->getLftName()), $grammar->wrap($this->parent->getRgtName())); - return $query->whereRaw($condition); /** @phpstan-ignore-line */ + return $query->whereRaw($condition); } /** @@ -137,11 +139,11 @@ public function getRelationCountHash($incrementJoinCount = true) /** * Get the results of the relationship. * - * @return Collection + * @return NestedSetCollection */ public function getResults() { - /** @var Collection */ + /** @disregard P1013 */ $result = $this->query->get(); return $result; @@ -164,6 +166,7 @@ public function addEagerConstraints(array $models) $this->query->whereNested(function (Builder $inner) use ($models) { // We will use this query in order to apply constraints to the // base query builder + /** @disregard P1013 */ $outer = $this->parent->newQuery()->setQuery($inner); foreach ($models as $model) { @@ -187,6 +190,7 @@ public function match(array $models, EloquentCollection $results, $relation) /** @disregard P1006 */ $related = $this->matchForModel($model, $results); + /** @disregard P1013 */ $model->setRelation($relation, $related); } @@ -197,16 +201,16 @@ public function match(array $models, EloquentCollection $results, $relation) * @param NodeModel $model * @param EloquentCollection $results * - * @return Collection + * @return NestedSetCollection */ protected function matchForModel(Model $model, EloquentCollection $results) { - /** @var Collection */ $result = $this->related->newCollection(); foreach ($results as $related) { /** @disregard P1006 */ if ($this->matches($model, $related)) { + /** @disregard P1013 */ $result->push($related); } } diff --git a/src/Collection.php b/src/Collection.php index de80624..cd45945 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -4,15 +4,19 @@ use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; +use Kalnoy\Nestedset\Contracts\NestedSetCollection; +use Kalnoy\Nestedset\Exceptions\NestedSetException; /** * @template Tmodel of Model * - * @phpstan-type NodeModel Node&Tmodel + * @phpstan-type NodeModel \Kalnoy\Nestedset\Contracts\Node&Model * * @extends EloquentCollection + * + * @implements NestedSetCollection */ -final class Collection extends EloquentCollection +final class Collection extends EloquentCollection implements NestedSetCollection { /** * Fill `parent` and `children` relationships for every node in the collection. @@ -34,16 +38,20 @@ public function linkNodes() /** @var NodeModel $node */ foreach ($this->items as $node) { if ($node->getParentId() === null) { + /** @disregard */ $node->setRelation('parent', null); } - /** @var array */ - $children = $groupedNodes->get($node->getKey(), []); /** @phpstan-ignore varTag.type */ + $children = $groupedNodes->get($node->getKey(), []); - foreach ($children as $child) { - $child->setRelation('parent', $node); + if (count($children) > 0) { + foreach ($children as $child) { + /** @disregard */ + $child->setRelation('parent', $node); + } } + /** @disregard */ $node->setRelation('children', EloquentCollection::make($children)); } @@ -59,9 +67,9 @@ public function linkNodes() * * @param mixed $root * - * @return Collection + * @return NestedSetCollection */ - public function toTree($root = false) + public function toTree($root = false): NestedSetCollection { if ($this->isEmpty()) { return new static(); @@ -123,23 +131,22 @@ protected function getRootNodeId($root = false) * * @param bool $root * - * @return Collection + * @return NestedSetCollection */ - public function toFlatTree($root = false): Collection + public function toFlatTree($root = false): NestedSetCollection { /** @Var Collection */ $result = new Collection(); if ($this->isEmpty()) { - return $result; /** @phpstan-ignore-line */ + return $result; } /** @var NodeModel */ $first = $this->first(); - /** @var Collection */ - $groupedNodes = $this->groupBy($first->getParentIdName()); /** @phpstan-ignore varTag.type */ + $groupedNodes = $this->groupBy($first->getParentIdName()); - return $result->flattenTree($groupedNodes, $this->getRootNodeId($root)); /** @phpstan-ignore-line */ + return $result->flattenTree($groupedNodes, $this->getRootNodeId($root)); } /** @@ -148,16 +155,18 @@ public function toFlatTree($root = false): Collection * @param Collection $groupedNodes * @param array-key $parentId * - * @return Collection + * @return NestedSetCollection */ - protected function flattenTree(Collection $groupedNodes, $parentId): Collection + protected function flattenTree(Collection $groupedNodes, $parentId): NestedSetCollection { - /** @var array */ - $nodes = $groupedNodes->get($parentId, []); /** @phpstan-ignore varTag.type */ - foreach ($nodes as $node) { - $this->push($node); + $nodes = $groupedNodes->get($parentId, []); - $this->flattenTree($groupedNodes, $node->getKey()); + if (count($nodes) > 0) { + foreach ($nodes as $node) { + $this->push($node); + + $this->flattenTree($groupedNodes, $node->getKey()); + } } return $this; diff --git a/src/Contracts/NestedSetCollection.php b/src/Contracts/NestedSetCollection.php new file mode 100644 index 0000000..e241719 --- /dev/null +++ b/src/Contracts/NestedSetCollection.php @@ -0,0 +1,48 @@ +&Tmodel + * + * @require-extends \Illuminate\Database\Eloquent\Collection + * + * @method NestedSetCollection groupBy(string $column) + * @method array all() + */ +interface NestedSetCollection +{ + /** + * Fill `parent` and `children` relationships for every node in the collection. + * + * This will overwrite any previously set relations. + * + * @return NestedSetCollection + */ + public function linkNodes(); + + /** + * Build a tree from a list of nodes. Each item will have set children relation. + * + * 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 NestedSetCollection + */ + public function toTree($root = false): NestedSetCollection; + + /** + * Build a list of nodes that retain the order that they were pulled from + * the database. + * + * @param bool $root + * + * @return NestedSetCollection + */ + public function toFlatTree($root = false): NestedSetCollection; +} diff --git a/src/Node.php b/src/Contracts/Node.php similarity index 69% rename from src/Node.php rename to src/Contracts/Node.php index fd2c0cc..5b7ddb0 100644 --- a/src/Node.php +++ b/src/Contracts/Node.php @@ -1,11 +1,11 @@ &Tmodel + * + * @require-extends \Illuminate\Database\Eloquent\Model + * + * @method mixed getKey() + * @method mixed getKeyName() + * @method Node setRelation($relation, $value) + * @method mixed save() + * @method string getTable() + * @method mixed getAttribute($key) + * @method string getDeletedAtColumn() + * @method Node getRelationValue($key) + * @method bool usesSoftDelete() */ interface Node { @@ -48,74 +60,74 @@ public function children(): HasMany; /** * Get query for descendants of the node. * - * @return DescendantsRelation + * @return Relation> */ - public function descendants(): DescendantsRelation; + public function descendants(): Relation; /** * Get query for siblings of the node. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function siblings(): QueryBuilder; + public function siblings(): NodeQueryBuilder; /** * Get the node siblings and the node itself. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function siblingsAndSelf(): QueryBuilder; + public function siblingsAndSelf(): NodeQueryBuilder; /** * Get query for the node siblings and the node itself. * * @param string[] $columns * - * @return EloquentCollection + * @return NestedSetCollection */ - public function getSiblingsAndSelf(array $columns = ['*']): EloquentCollection; + public function getSiblingsAndSelf(array $columns = ['*']): NestedSetCollection; /** * Get query for siblings after the node. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function nextSiblings(): QueryBuilder; + public function nextSiblings(): NodeQueryBuilder; /** * Get query for siblings before the node. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function prevSiblings(): QueryBuilder; + public function prevSiblings(): NodeQueryBuilder; /** * Get query for nodes after current node. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function nextNodes(): QueryBuilder; + public function nextNodes(): NodeQueryBuilder; /** * Get query for nodes before current node in reversed order. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function prevNodes(): QueryBuilder; + public function prevNodes(): NodeQueryBuilder; /** * Get query ancestors of the node. * - * @return AncestorsRelation + * @return Relation> */ - public function ancestors(): AncestorsRelation; + public function ancestors(): Relation; /** * Make this node a root node. * * @return $this */ - public function makeRoot(): Node; + public function makeRoot(): self; /** * Save node as root. @@ -131,7 +143,7 @@ public function saveAsRoot(): bool; * * @return $this */ - public function rawNode(int $lft, int $rgt, mixed $parentId): Node; + public function rawNode(int $lft, int $rgt, mixed $parentId): self; /** * Move node up given amount of positions. @@ -154,27 +166,27 @@ public function down(int $amount = 1): bool; /** * @since 2.0 * - * @param BaseQueryBuilder|EloquentBuilder|QueryBuilder $query + * @param BaseQueryBuilder|EloquentBuilder|NodeQueryBuilder $query * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function newEloquentBuilder(BaseQueryBuilder|EloquentBuilder|QueryBuilder $query): QueryBuilder; + public function newEloquentBuilder(BaseQueryBuilder|EloquentBuilder|NodeQueryBuilder $query): NodeQueryBuilder; /** * Get a new base query that includes deleted nodes. * * @since 1.1 * - * @param (QueryBuilder)|string|null $table + * @param NodeQueryBuilder|string|null $table * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function newNestedSetQuery(QueryBuilder|string|null $table = null): QueryBuilder; + public function newNestedSetQuery(NodeQueryBuilder|string|null $table = null): NodeQueryBuilder; /** * @param ?string $table * - * @return QueryBuilder + * @return NodeQueryBuilder */ public function newScopedQuery($table = null); @@ -189,16 +201,16 @@ public function applyNestedSetScope($query, $table = null); /** * @param string[] $attributes * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public static function scoped(array $attributes): QueryBuilder; + public static function scoped(array $attributes): NodeQueryBuilder; /** * @param array $models * - * @return Collection + * @return NestedSetCollection */ - public function newCollection(array $models = []): Collection; + public function newCollection(array $models = []): NestedSetCollection; /** * Get node height (rgt - lft + 1). @@ -226,6 +238,9 @@ public function setParentIdAttribute(mixed $value): void; */ public function isRoot(): bool; + /** + * Get whether node is a leaf. + */ public function isLeaf(): bool; /** @@ -285,35 +300,35 @@ public function getPrevNode(array $columns = ['*']): Node; /** * @param string[] $columns * - * @return Collection + * @return NestedSetCollection */ public function getAncestors(array $columns = ['*']); /** * @param string[] $columns * - * @return Collection + * @return NestedSetCollection */ public function getDescendants(array $columns = ['*']); /** * @param string[] $columns * - * @return Collection + * @return NestedSetCollection */ public function getSiblings(array $columns = ['*']); /** * @param string[] $columns * - * @return Collection + * @return NestedSetCollection */ public function getNextSiblings(array $columns = ['*']); /** * @param string[] $columns * - * @return Collection + * @return NestedSetCollection */ public function getPrevSiblings(array $columns = ['*']); @@ -322,14 +337,14 @@ public function getPrevSiblings(array $columns = ['*']); * * @return NodeModel */ - public function getNextSibling(array $columns = ['*']); + public function getNextSibling(array $columns = ['*']): Node; /** * @param string[] $columns * * @return NodeModel */ - public function getPrevSibling(array $columns = ['*']); + public function getPrevSibling(array $columns = ['*']): Node; /** * @return array @@ -341,21 +356,21 @@ public function getBounds(); * * @return NodeModel */ - public function setLft(int $value): Node; + public function setLft(int $value): self; /** * @param $value * * @return NodeModel */ - public function setRgt(int $value): Node; + public function setRgt(int $value): self; /** * @param array-key|null $id * * @return NodeModel */ - public function setParentId(mixed $id): Node; + public function setParentId(mixed $id): self; /** * @param string[]|null $except @@ -380,7 +395,7 @@ public function appendNode(Node $node): bool; * * @return NodeModel */ - public function appendToNode(Node $parent): Node; + public function appendToNode(Node $parent): self; /** * Prepend a node to the new parent. @@ -389,7 +404,7 @@ public function appendToNode(Node $parent): Node; * * @return NodeModel */ - public function prependToNode(Node $parent): Node; + public function prependToNode(Node $parent): self; /** * Get whether the node is an ancestor of other node, including immediate parent. @@ -403,7 +418,7 @@ public function isAncestorOf(Node $other): bool; /** * Get whether a node is a descendant of other node. * - * @param NodeModel $other + * @param Node $other * * @return bool */ @@ -421,7 +436,7 @@ public function isSelfOrDescendantOf(Node $other); /** * Create a new Query. * - * @return QueryBuilder + * @return NodeQueryBuilder */ public function newQuery(); } diff --git a/src/Contracts/NodeQueryBuilder.php b/src/Contracts/NodeQueryBuilder.php new file mode 100644 index 0000000..e5787ff --- /dev/null +++ b/src/Contracts/NodeQueryBuilder.php @@ -0,0 +1,395 @@ + + * + * @require-extends Illuminate\Database\Eloquent\Builder + * + * @method NodeQueryBuilder select(string[] $columns) + * @method Tmodel getModel() + * @method NodeQueryBuilder from(string $table) + * @method NodeQueryBuilder getQuery() + * @method NodeQueryBuilder whereRaw(string $sql, string[] $bindings = [], string $boolean = 'and') + * @method \Illuminate\Database\Query\Grammars\Grammar getGrammar() + * @method NodeQueryBuilder whereNested(\Closure|string $callback, string $boolean = 'and') + * @method NestedSetCollection get(string[] $columns = ['*']) + * @method int max(string $column) + * @method NodeQueryBuilder where(string|string[]|\Closure $column, mixed $operator = null, mixed $value = null, string $boolean = 'and') + * @method NodeModel|null first(string[]|string $columns = ['*']) + * @method NodeModel findOrFail(int|string $id) + * @method NodeQueryBuilder skip(int $value) + * @method NodeQueryBuilder take(int $value) + * @method NodeQueryBuilder orderBy(string $column, string $direction = 'asc') + * @method NodeQueryBuilder when(bool $value, \Closure $callback) + * @method BaseQueryBuilder toBase() + */ +interface NodeQueryBuilder extends Builder +{ + /** + * Get node's `lft` and `rgt` values. + * + * @since 2.0 + * + * @param NodeModel $id + * @param bool $required + * + * @return array + */ + public function getNodeData(mixed $id, $required = false): array; + + /** + * Get plain node data. + * + * @since 2.0 + * + * @param NodeModel $id + * @param bool $required + * + * @return array + */ + public function getPlainNodeData(mixed $id, $required = false): array; + + /** + * Scope limits query to select just root node. + * + * @return NodeQueryBuilder + */ + public function whereIsRoot(): NodeQueryBuilder; + + /** + * Limit results to ancestors of specified node. + * + * @since 2.0 + * + * @param NodeModel $id + * @param bool $andSelf + * @param string $boolean + * + * @return NodeQueryBuilder + */ + public function whereAncestorOf(mixed $id, bool $andSelf = false, string $boolean = 'and'): NodeQueryBuilder; + + /** + * @param NodeModel $id + * @param bool $andSelf + * + * @return NodeQueryBuilder + */ + public function orWhereAncestorOf(mixed $id, $andSelf = false): NodeQueryBuilder; + + /** + * @param NodeModel $id + * + * @return NodeQueryBuilder + */ + public function whereAncestorOrSelf(mixed $id): NodeQueryBuilder; + + /** + * Get ancestors of specified node. + * + * @since 2.0 + * + * @param NodeModel $id + * @param string[] $columns + * + * @return NestedSetCollection + */ + public function ancestorsOf(mixed $id, array $columns = ['*']): NestedSetCollection; + + /** + * @param NodeModel $id + * @param string[] $columns + * + * @return NestedSetCollection + */ + public function ancestorsAndSelf(mixed $id, array $columns = ['*']): NestedSetCollection; + + /** + * Add node selection statement between specified range. + * + * @since 2.0 + * + * @param int[] $values + * @param string $boolean + * @param bool $not + * + * @return NodeQueryBuilder + */ + public function whereNodeBetween(array $values, $boolean = 'and', $not = false): NodeQueryBuilder; + + /** + * Add node selection statement between specified range joined with `or` operator. + * + * @since 2.0 + * + * @param int[] $values + * + * @return NodeQueryBuilder + */ + public function orWhereNodeBetween(array $values): NodeQueryBuilder; + + /** + * Add constraint statement to descendants of specified node. + * + * @since 2.0 + * + * @param ?NodeModel $id + * @param string $boolean + * @param bool $not + * @param bool $andSelf + * + * @return NodeQueryBuilder + */ + public function whereDescendantOf(mixed $id, $boolean = 'and', $not = false, $andSelf = false): NodeQueryBuilder; + + /** + * @param NodeModel $id + * + * @return NodeQueryBuilder + */ + public function whereNotDescendantOf(mixed $id): NodeQueryBuilder; + + /** + * @param NodeModel $id + * + * @return NodeQueryBuilder + */ + public function orWhereDescendantOf(mixed $id): NodeQueryBuilder; + + /** + * @param NodeModel $id + * + * @return NodeQueryBuilder + */ + public function orWhereNotDescendantOf(mixed $id): NodeQueryBuilder; + + /** + * @param NodeModel $id + * @param string $boolean + * @param bool $not + * + * @return NodeQueryBuilder + */ + public function whereDescendantOrSelf(mixed $id, string $boolean = 'and', bool $not = false): NodeQueryBuilder; + + /** + * Get descendants of specified node. + * + * @since 2.0 + * + * @param NodeModel $id + * @param string[] $columns + * @param bool $andSelf + * + * @return NestedSetCollection + */ + public function descendantsOf(mixed $id, array $columns = ['*'], bool $andSelf = false): NestedSetCollection; + + /** + * @param NodeModel $id + * @param string[] $columns + * + * @return NestedSetCollection + */ + public function descendantsAndSelf($id, array $columns = ['*']): NestedSetCollection; + + /** + * Constraint nodes to those that are after specified node. + * + * @since 2.0 + * + * @param NodeModel $id + * @param string $boolean + * + * @return NodeQueryBuilder + */ + public function whereIsAfter($id, $boolean = 'and'): NodeQueryBuilder; + + /** + * Constraint nodes to those that are before specified node. + * + * @since 2.0 + * + * @param NodeModel $id + * @param string $boolean + * + * @return NodeQueryBuilder + */ + public function whereIsBefore($id, $boolean = 'and'): NodeQueryBuilder; + + /** + * @return NodeQueryBuilder + */ + public function whereIsLeaf(): NodeQueryBuilder; + + /** + * @param string[] $columns + * + * @return NestedSetCollection + */ + public function leaves(array $columns = ['*']): NestedSetCollection; + + /** + * Include depth level into the result. + * + * @param string $as + * + * @return NodeQueryBuilder + */ + public function withDepth($as = 'depth'): NodeQueryBuilder; + + /** + * Exclude root node from the result. + * + * @return NodeQueryBuilder + */ + public function withoutRoot(): NodeQueryBuilder; + + /** + * Equivalent of `withoutRoot`. + * + * @since 2.0 + * @deprecated since v4.1 + * + * @return NodeQueryBuilder + */ + public function hasParent(): NodeQueryBuilder; + + /** + * Get only nodes that have children. + * + * @since 2.0 + * @deprecated since v4.1 + * + * @return NodeQueryBuilder + */ + public function hasChildren(): NodeQueryBuilder; + + /** + * Order by node position. + * + * @param string $dir + * + * @return NodeQueryBuilder + */ + public function defaultOrder($dir = 'asc'): NodeQueryBuilder; + + /** + * Order by reversed node position. + * + * @return NodeQueryBuilder + */ + public function reversed(): NodeQueryBuilder; + + /** + * Move a node to the new position. + * + * @param mixed $key + * @param int $position + * + * @return int + */ + public function moveNode($key, $position): int; + + /** + * 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): int; + + /** + * Get statistics of errors of the tree. + * + * @since 2.0 + * + * @return array{oddness:int,duplicates:int,wrong_parent:int,missing_parent:int} + */ + public function countErrors(): array; + + /** + * Get the number of total errors of the tree. + * + * @since 2.0 + * + * @return int + */ + public function getTotalErrors(): int; + + /** + * Get whether the tree is broken. + * + * @since 2.0 + * + * @return bool + */ + public function isBroken(): bool; + + /** + * Fixes the tree based on parentage info. + * + * Nodes with invalid parent are saved as roots. + * + * @param ?NodeModel $root + * + * @return int The number of changed nodes + */ + public function fixTree($root = null): int; + + /** + * @param NodeModel $root + * + * @return int + */ + public function fixSubtree($root): int; + + /** + * Rebuild the tree based on raw data. + * + * 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 ?NodeModel $root + * + * @return int + */ + public function rebuildTree(array $data, $delete = false, $root = null): int; + + /** + * @param null $root + * @param array[] $data + * @param bool $delete + * + * @return int + */ + public function rebuildSubtree($root, array $data, $delete = false): int; + + /** + * @param string|null $table + * + * @return NodeQueryBuilder + */ + public function applyNestedSetScope($table = null): NodeQueryBuilder; + + /** + * Get the root node. + * + * @param string[] $columns + * + * @return NodeModel|null + */ + public function root(array $columns = ['*']): Node|null; +} \ No newline at end of file diff --git a/src/DescendantsRelation.php b/src/DescendantsRelation.php index 2e13bcc..02ed7bb 100644 --- a/src/DescendantsRelation.php +++ b/src/DescendantsRelation.php @@ -3,17 +3,18 @@ namespace Kalnoy\Nestedset; use Illuminate\Database\Eloquent\Model; +use Kalnoy\Nestedset\Contracts\Node; /** * @template Tmodel of Model * - * @phpstan-type NodeModel Node&Tmodel + * @phpstan-type NodeModel \Kalnoy\Nestedset\Contracts\Node * * @disregard P1037 * * @extends BaseRelation */ -class DescendantsRelation extends BaseRelation +final class DescendantsRelation extends BaseRelation { /** * Set the base constraints on the relation query. @@ -45,7 +46,7 @@ protected function addEagerConstraint($query, $model) * * @return bool */ - protected function matches(Model $model, $related): bool + protected function matches(Node $model, Node $related): bool { return $related->isDescendantOf($model); } diff --git a/src/NestedSetException.php b/src/Exceptions/NestedSetException.php similarity index 86% rename from src/NestedSetException.php rename to src/Exceptions/NestedSetException.php index 38bad44..2d31573 100644 --- a/src/NestedSetException.php +++ b/src/Exceptions/NestedSetException.php @@ -1,6 +1,6 @@ |null */ protected array|null $pending = null; @@ -49,11 +59,11 @@ public static function bootNodeTrait(): void }); if (static::usesSoftDelete()) { - static::restoring(function ($model) { - static::$deletedAt = $model->{$model->getDeletedAtColumn()}; + static::restoring(function ($model) { /** @phpstan-ignore staticMethod.notFound */ + static::$deletedAt = $model->{$model->getDeletedAtColumn()}; /** @phpstan-ignore property.dynamicName */ }); - static::restored(function ($model) { + static::restored(function ($model) { /** @phpstan-ignore staticMethod.notFound */ $model->restoreDescendants(static::$deletedAt); }); } @@ -64,9 +74,9 @@ public static function bootNodeTrait(): void * * @param string $action * - * @return $this + * @return self */ - protected function setNodeAction($action): Node + protected function setNodeAction($action): self { $this->pending = func_get_args(); @@ -76,15 +86,15 @@ protected function setNodeAction($action): Node /** * Call pending action. */ - protected function callPendingAction() + protected function callPendingAction(): void { $this->moved = false; - if (!$this->pending && !$this->exists) { + if (($this->pending === null || $this->pending === []) && !$this->exists) { $this->makeRoot(); } - if (!$this->pending) { + if ($this->pending === null || $this->pending === []) { return; } @@ -104,9 +114,9 @@ public static function usesSoftDelete(): bool static $softDelete; if (is_null($softDelete)) { - $instance = new static(); + $instance = new self(); - return $softDelete = method_exists($instance, 'bootSoftDeletes'); + return $softDelete = method_exists($instance, 'bootSoftDeletes'); /** @phpstan-ignore function.alreadyNarrowedType, function.impossibleType */ } return $softDelete; @@ -123,7 +133,7 @@ protected function actionRaw(): bool /** * Make a root node. */ - protected function actionRoot() + protected function actionRoot(): bool { // Simplest case that do not affect other nodes. if (!$this->exists) { @@ -143,18 +153,18 @@ protected function actionRoot() */ protected function getLowerBound(): int { - return (int) $this->newNestedSetQuery()->max($this->getRgtName()); + return (int) $this->newNestedSetQuery()->max($this->getRgtName()); /** @phpstan-ignore staticMethod.dynamicCall, cast.useless */ } /** * Append or prepend a node to the parent. * - * @param Node $parent - * @param bool $prepend + * @param Node $parent + * @param bool $prepend * * @return bool */ - protected function actionAppendOrPrepend(Node $parent, $prepend = false) + protected function actionAppendOrPrepend(Node $parent, $prepend = false): bool { $parent->refreshNode(); @@ -172,13 +182,13 @@ protected function actionAppendOrPrepend(Node $parent, $prepend = false) /** * Apply parent model. * - * @param Model|null $value + * @param Node|null $value * - * @return $this + * @return self */ protected function setParent($value) { - $this->setParentId($value ? $value->getKey() : null) + $this->setParentId($value !== null ? $value->getKey() : null) ->setRelation('parent', $value); return $this; @@ -187,8 +197,8 @@ protected function setParent($value) /** * Insert node before or after another node. * - * @param Node $node - * @param bool $after + * @param Node $node + * @param bool $after * * @return bool */ @@ -217,31 +227,31 @@ public function refreshNode(): void /** * Relation to the parent. * - * @return BelongsTo + * @return BelongsTo */ public function parent(): BelongsTo { - return $this->belongsTo(get_class($this), $this->getParentIdName()) + return $this->belongsTo(get_class($this), $this->getParentIdName()) /** @phpstan-ignore-line staticMethod.dynamicCall */ ->setModel($this); } /** * Relation to children. * - * @return HasMany + * @return HasMany */ public function children(): HasMany { - return $this->hasMany(get_class($this), $this->getParentIdName()) + return $this->hasMany(get_class($this), $this->getParentIdName()) /** @phpstan-ignore-line staticMethod.dynamicCall */ ->setModel($this); } /** * Get query for descendants of the node. * - * @return DescendantsRelation + * @return Relation> */ - public function descendants(): DescendantsRelation + public function descendants(): Relation { return new DescendantsRelation($this->newQuery(), $this); } @@ -249,9 +259,9 @@ public function descendants(): DescendantsRelation /** * Get query for siblings of the node. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function siblings(): QueryBuilder + public function siblings(): NodeQueryBuilder { return $this->newScopedQuery() ->where($this->getKeyName(), '<>', $this->getKey()) @@ -261,9 +271,9 @@ public function siblings(): QueryBuilder /** * Get the node siblings and the node itself. * - * @return \Kalnoy\Nestedset\QueryBuilder + * @return NodeQueryBuilder */ - public function siblingsAndSelf(): QueryBuilder + public function siblingsAndSelf(): NodeQueryBuilder { return $this->newScopedQuery() ->where($this->getParentIdName(), '=', $this->getParentId()); @@ -272,11 +282,11 @@ public function siblingsAndSelf(): QueryBuilder /** * Get query for the node siblings and the node itself. * - * @param array $columns + * @param string[] $columns * - * @return \Illuminate\Database\Eloquent\Collection + * @return NestedSetCollection */ - public function getSiblingsAndSelf(array $columns = ['*']): EloquentCollection + public function getSiblingsAndSelf(array $columns = ['*']): NestedSetCollection { return $this->siblingsAndSelf()->get($columns); } @@ -284,9 +294,9 @@ public function getSiblingsAndSelf(array $columns = ['*']): EloquentCollection /** * Get query for siblings after the node. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function nextSiblings(): QueryBuilder + public function nextSiblings(): NodeQueryBuilder { return $this->nextNodes() ->where($this->getParentIdName(), '=', $this->getParentId()); @@ -295,9 +305,9 @@ public function nextSiblings(): QueryBuilder /** * Get query for siblings before the node. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function prevSiblings(): QueryBuilder + public function prevSiblings(): NodeQueryBuilder { return $this->prevNodes() ->where($this->getParentIdName(), '=', $this->getParentId()); @@ -306,9 +316,9 @@ public function prevSiblings(): QueryBuilder /** * Get query for nodes after current node. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function nextNodes(): QueryBuilder + public function nextNodes(): NodeQueryBuilder { return $this->newScopedQuery() ->where($this->getLftName(), '>', $this->getLft()); @@ -317,20 +327,20 @@ public function nextNodes(): QueryBuilder /** * Get query for nodes before current node in reversed order. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function prevNodes(): QueryBuilder + public function prevNodes(): NodeQueryBuilder { return $this->newScopedQuery() - ->where($this->getLftName(), '<', $this->getLft()); + ->where($this->getLftName(), '<', $this->getLft()); /** @phpstan-ignore method.notFound */ } /** * Get query ancestors of the node. * - * @return AncestorsRelation + * @return Relation> */ - public function ancestors(): AncestorsRelation + public function ancestors(): Relation { return new AncestorsRelation($this->newQuery(), $this); } @@ -338,7 +348,7 @@ public function ancestors(): AncestorsRelation /** * Make this node a root node. * - * @return $this + * @return Node */ public function makeRoot(): Node { @@ -364,7 +374,7 @@ public function saveAsRoot(): bool /** * Append and save a node. * - * @param Node&Tmodel $node + * @param Node $node * * @return bool */ @@ -377,7 +387,7 @@ public function appendNode(Node $node): bool /** * Prepend and save a node. * - * @param Node $node + * @param Node $node * * @return bool */ @@ -389,11 +399,11 @@ public function prependNode(Node $node): bool /** * Append a node to the new parent. * - * @param Node&Tmodel $parent + * @param Node $parent * - * @return Node&Tmodel + * @return self */ - public function appendToNode(Node $parent): Node + public function appendToNode(Node $parent): self { return $this->appendOrPrependTo($parent); } @@ -401,22 +411,22 @@ public function appendToNode(Node $parent): Node /** * Prepend a node to the new parent. * - * @param Node $parent + * @param Node $parent * * @return $this */ public function prependToNode(Node $parent): Node { - return $this->appendOrPrependTo($parent, true); + return $this->appendOrPrependTo($parent, true); /** @phpstan-ignore return.type */ } /** - * @param Node $parent - * @param bool $prepend + * @param Node $parent + * @param bool $prepend * - * @return Node + * @return self */ - public function appendOrPrependTo(Node $parent, bool $prepend = false) + protected function appendOrPrependTo(Node $parent, bool $prepend = false): self { $this->assertNodeExists($parent) ->assertNotDescendant($parent) @@ -430,32 +440,32 @@ public function appendOrPrependTo(Node $parent, bool $prepend = false) /** * Insert self after a node. * - * @param Node $node + * @param Node $node * * @return $this */ public function afterNode(Node $node) { - return $this->beforeOrAfterNode($node, true); + return $this->beforeOrAfterNode($node, true); /** @phpstan-ignore return.type */ } /** * Insert self before node. * - * @param Node $node + * @param Node $node * * @return $this */ public function beforeNode(Node $node) { - return $this->beforeOrAfterNode($node); + return $this->beforeOrAfterNode($node); /** @phpstan-ignore return.type */ } /** - * @param Node&Tmodel $node - * @param bool $after + * @param Node $node + * @param bool $after * - * @return Node + * @return Node */ public function beforeOrAfterNode(Node $node, bool $after = false) { @@ -475,7 +485,7 @@ public function beforeOrAfterNode(Node $node, bool $after = false) /** * Insert self after a node and save. * - * @param Node $node + * @param Node $node * * @return bool */ @@ -487,7 +497,7 @@ public function insertAfterNode(Node $node) /** * Insert self before a node and save. * - * @param Node $node + * @param Node $node * * @return bool */ @@ -508,7 +518,7 @@ public function insertBeforeNode(Node $node) * @param int $rgt * @param Tmodelkey|null $parentId * - * @return Node + * @return Node */ public function rawNode(int $lft, int $rgt, mixed $parentId): Node { @@ -531,7 +541,7 @@ public function up(int $amount = 1): bool ->skip($amount - 1) ->first(); - if (!$sibling) { + if ($sibling === null) { return false; } @@ -552,7 +562,7 @@ public function down(int $amount = 1): bool ->skip($amount - 1) ->first(); - if (!$sibling) { + if ($sibling === null) { return false; } @@ -584,9 +594,9 @@ protected function insertAt($position) * * @param int $position * - * @return int + * @return bool */ - protected function moveNode(int $position) + protected function moveNode(int $position): bool { $updated = $this->newNestedSetQuery() ->moveNode($this->getKey(), $position) > 0; @@ -607,7 +617,7 @@ protected function moveNode(int $position) * * @return bool */ - protected function insertNode(int $position) + protected function insertNode(int $position): bool { $this->newNestedSetQuery()->makeGap($position, 2); @@ -622,12 +632,12 @@ protected function insertNode(int $position) /** * Update the tree when the node is removed physically. */ - protected function deleteDescendants() + protected function deleteDescendants(): void { $lft = $this->getLft(); $rgt = $this->getRgt(); - $method = $this->usesSoftDelete() && $this->forceDeleting + $method = $this->usesSoftDelete() && $this->forceDeleting /** @phpstan-ignore property.notFound, staticMethod.dynamicCall */ ? 'forceDelete' : 'delete'; @@ -653,6 +663,7 @@ protected function deleteDescendants() // need for it. // The grammar compiler removes the superfluous "ORDER BY" for // PostgreSQL. + /** @phpstan-ignore method.dynamicName, staticMethod.dynamicCall */ $this->descendants() ->orderBy($this->getLftName(), 'desc') ->{$method}(); @@ -672,23 +683,24 @@ protected function deleteDescendants() /** * Restore the descendants. * - * @param $deletedAt + * @param Carbon $deletedAt */ - protected function restoreDescendants($deletedAt) + protected function restoreDescendants(Carbon $deletedAt): void { - $this->descendants() + $this->descendants() /** @phpstan-ignore staticMethod.dynamicCall, method.notFound */ ->where($this->getDeletedAtColumn(), '>=', $deletedAt) ->restore(); } /** - * @param BaseQueryBuilder|EloquentBuilder|QueryBuilder $query + * @param BaseQueryBuilder|EloquentBuilder|QueryBuilder $query * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function newEloquentBuilder($query): QueryBuilder + public function newEloquentBuilder($query): NodeQueryBuilder { /** @disregard P1006 */ + /** @var QueryBuilder */ return new QueryBuilder($query); } @@ -697,12 +709,13 @@ public function newEloquentBuilder($query): QueryBuilder * * @since 1.1 * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function newNestedSetQuery($table = null): QueryBuilder + public function newNestedSetQuery($table = null): NodeQueryBuilder { + /** @phpstan-ignore staticMethod.dynamicCall */ $builder = $this->usesSoftDelete() - ? $this->withTrashed() + ? $this->withTrashed() /** @phpstan-ignore method.notFound, staticMethod.dynamicCall (it does exists!) */ : $this->newQuery(); return $this->applyNestedSetScope($builder, $table); @@ -711,9 +724,9 @@ public function newNestedSetQuery($table = null): QueryBuilder /** * @param string $table * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function newScopedQuery($table = null): QueryBuilder + public function newScopedQuery($table = null): NodeQueryBuilder { return $this->applyNestedSetScope($this->newQuery(), $table); } @@ -726,11 +739,12 @@ public function newScopedQuery($table = null): QueryBuilder */ public function applyNestedSetScope($query, $table = null) { - if (!$scoped = $this->getScopeAttributes()) { + $scoped = $this->getScopeAttributes(); + if ($scoped === null || $scoped === []) { /** @phpstan-ignore identical.alwaysFalse */ return $query; } - if (!$table) { + if ($table === null) { $table = $this->getTable(); } @@ -743,7 +757,7 @@ public function applyNestedSetScope($query, $table = null) } /** - * @return array + * @return string[]|null */ protected function getScopeAttributes() { @@ -751,13 +765,13 @@ protected function getScopeAttributes() } /** - * @param array $attributes + * @param string[] $attributes * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public static function scoped(array $attributes): QueryBuilder + public static function scoped(array $attributes): NodeQueryBuilder { - $instance = new static(); + $instance = new self(); $instance->setRawAttributes($attributes); @@ -765,9 +779,9 @@ public static function scoped(array $attributes): QueryBuilder } /** - * {@inheritdoc} + * @return NestedSetCollection */ - public function newCollection(array $models = []): Collection + public function newCollection(array $models = []): NestedSetCollection { return new Collection($models); } @@ -777,15 +791,18 @@ public function newCollection(array $models = []): Collection * * Use `children` key on `$attributes` to create child nodes. * - * @param Node $parent + * @param array $attributes + * @param Node $parent + * + * @return Node */ public static function create(array $attributes = [], ?Node $parent = null) { $children = Arr::pull($attributes, 'children'); - $instance = new static($attributes); + $instance = new self($attributes); - if ($parent) { + if ($parent !== null) { $instance->appendToNode($parent); } @@ -801,8 +818,9 @@ public static function create(array $attributes = [], ?Node $parent = null) } $instance->refreshNode(); + $instance->setRelation('children', $relation); - return $instance->setRelation('children', $relation); + return $instance; } /** @@ -822,7 +840,7 @@ public function getNodeHeight(): int */ public function getDescendantCount(): int { - return ceil($this->getNodeHeight() / 2) - 1; + return (int) ceil($this->getNodeHeight() / 2) - 1; } /** @@ -836,12 +854,11 @@ public function getDescendantCount(): int */ public function setParentIdAttribute(mixed $value): void { - if ($this->getParentId() == $value) { + if ($this->getParentId() === $value) { return; } - if ($value) { - /** @var Node&Tmodel */ + if ($value !== null) { $node = $this->newScopedQuery()->findOrFail($value); $this->appendToNode($node); } else { @@ -859,7 +876,7 @@ public function isRoot(): bool public function isLeaf(): bool { - return $this->getLft() + 1 == $this->getRgt(); + return $this->getLft() + 1 === $this->getRgt(); } /** @@ -919,7 +936,7 @@ public function getParentId(): mixed * * @param string[] $columns * - * @return Node + * @return Node */ public function getNextNode(array $columns = ['*']): Node { @@ -933,7 +950,7 @@ public function getNextNode(array $columns = ['*']): Node * * @param string[] $columns * - * @return Node + * @return Node */ public function getPrevNode(array $columns = ['*']): Node { @@ -943,27 +960,27 @@ public function getPrevNode(array $columns = ['*']): Node /** * @param string[] $columns * - * @return Collection + * @return NestedSetCollection */ public function getAncestors(array $columns = ['*']) { - return $this->ancestors()->get($columns); + return $this->ancestors()->get($columns); /** @phpstan-ignore return.type */ } /** * @param string[] $columns * - * @return Collection|Node[] + * @return NestedSetCollection */ public function getDescendants(array $columns = ['*']) { - return $this->descendants()->get($columns); + return $this->descendants()->get($columns); /** @phpstan-ignore return.type */ } /** * @param string[] $columns * - * @return Collection|Node[] + * @return NestedSetCollection */ public function getSiblings(array $columns = ['*']) { @@ -973,7 +990,7 @@ public function getSiblings(array $columns = ['*']) /** * @param string[] $columns * - * @return Collection|Node[] + * @return NestedSetCollection */ public function getNextSiblings(array $columns = ['*']) { @@ -983,7 +1000,7 @@ public function getNextSiblings(array $columns = ['*']) /** * @param string[] $columns * - * @return Collection|Node[] + * @return NestedSetCollection */ public function getPrevSiblings(array $columns = ['*']) { @@ -993,9 +1010,9 @@ public function getPrevSiblings(array $columns = ['*']) /** * @param string[] $columns * - * @return Node + * @return Node */ - public function getNextSibling(array $columns = ['*']) + public function getNextSibling(array $columns = ['*']): Node { return $this->nextSiblings()->defaultOrder()->first($columns); } @@ -1003,9 +1020,9 @@ public function getNextSibling(array $columns = ['*']) /** * @param string[] $columns * - * @return Node + * @return Node */ - public function getPrevSibling(array $columns = ['*']) + public function getPrevSibling(array $columns = ['*']): Node { return $this->prevSiblings()->defaultOrder('desc')->first($columns); } @@ -1013,7 +1030,7 @@ public function getPrevSibling(array $columns = ['*']) /** * Get whether a node is a descendant of other node. * - * @param Node $other + * @param Node $other * * @return bool */ @@ -1026,7 +1043,7 @@ public function isDescendantOf(Node $other): bool /** * Get whether a node is itself or a descendant of other node. * - * @param Node $other + * @param Node $other * * @return bool */ @@ -1045,25 +1062,25 @@ public function isSelfOrDescendantOf(Node $other) */ public function isChildOf(Node $other) { - return $this->getParentId() == $other->getKey(); + return $this->getParentId() === $other->getKey(); } /** * Get whether the node is a sibling of another node. * - * @param Node $other + * @param Node $other * * @return bool */ public function isSiblingOf(Node $other) { - return $this->getParentId() == $other->getParentId(); + return $this->getParentId() === $other->getParentId(); } /** * Get whether the node is an ancestor of other node, including immediate parent. * - * @param Node $other + * @param Node $other * * @return bool */ @@ -1075,7 +1092,7 @@ public function isAncestorOf(Node $other): bool /** * Get whether the node is itself or an ancestor of other node, including immediate parent. * - * @param Node $other + * @param Node $other * * @return bool */ @@ -1095,7 +1112,7 @@ public function hasMoved() } /** - * @return array + * @return string[] */ protected function getArrayableRelations() { @@ -1114,11 +1131,11 @@ protected function getArrayableRelations() */ protected function hardDeleting() { - return !$this->usesSoftDelete() || $this->forceDeleting; + return !$this->usesSoftDelete() || $this->forceDeleting; /** @phpstan-ignore property.notFound, staticMethod.dynamicCall */ } /** - * @return array + * @return array{0:int,1:int} */ public function getBounds() { @@ -1128,7 +1145,7 @@ public function getBounds() /** * @param $value * - * @return Node $this + * @return Node $this */ public function setLft(int $value): Node { @@ -1140,7 +1157,7 @@ public function setLft(int $value): Node /** * @param $value * - * @return Node $this + * @return Node $this */ public function setRgt(int $value): Node { @@ -1152,7 +1169,7 @@ public function setRgt(int $value): Node /** * @param Tmodelkey|null $value * - * @return Node&Tmodel + * @return Node&Tmodel */ public function setParentId(mixed $value): Node { @@ -1162,7 +1179,7 @@ public function setParentId(mixed $value): Node } /** - * @return Node + * @return Node */ protected function dirtyBounds() { @@ -1173,13 +1190,13 @@ protected function dirtyBounds() } /** - * @param Node $node + * @param Node $node * - * @return Node + * @return Node */ protected function assertNotDescendant(Node $node) { - if ($node == $this || $node->isDescendantOf($this)) { + if ($node === $this || $node->isDescendantOf($this)) { throw new \LogicException('Node must not be a descendant.'); } @@ -1187,13 +1204,13 @@ protected function assertNotDescendant(Node $node) } /** - * @param Node $node + * @param Node $node * - * @return Node&Tmodel + * @return Node&Tmodel */ protected function assertNodeExists(Node $node) { - if (!$node->getLft() || !$node->getRgt()) { + if ($node->getLft() === null || $node->getRgt() === null) { throw new \LogicException('Node must exists.'); } @@ -1201,16 +1218,19 @@ protected function assertNodeExists(Node $node) } /** - * @param Node&Tmodel $node + * @param Node&Tmodel $node + * + * @phpstan-ignore generics.notSubtype */ protected function assertSameScope(Node $node): void { - if (!$scoped = $this->getScopeAttributes()) { + $scoped = $this->getScopeAttributes(); + if ($scoped === null || $scoped === []) { /** @phpstan-ignore identical.alwaysFalse */ return; } foreach ($scoped as $attr) { - if ($this->getAttribute($attr) != $node->getAttribute($attr)) { + if ($this->getAttribute($attr) !== $node->getAttribute($attr)) { throw new \LogicException('Nodes must be in the same scope'); } } @@ -1219,7 +1239,9 @@ protected function assertSameScope(Node $node): void /** * @param string[]|null $except * - * @return \Illuminate\Database\Eloquent\Model + * @return Node&Tmodel + * + * @phpstan-ignore generics.notSubtype */ public function replicate(?array $except = null): Node { @@ -1229,7 +1251,7 @@ public function replicate(?array $except = null): Node $this->getRgtName(), ]; - $except = $except ? array_unique(array_merge($except, $defaults)) : $defaults; + $except = ($except !== null && $except !== []) ? array_unique(array_merge($except, $defaults)) : $defaults; return parent::replicate($except); } diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 2288098..3e4cbd4 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -3,13 +3,16 @@ 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; use Illuminate\Database\Query\Builder as Query; use Illuminate\Database\Query\Expression; use Illuminate\Support\Arr; +use Kalnoy\Nestedset\Contracts\NestedSetCollection; +use Kalnoy\Nestedset\Contracts\Node; +use Kalnoy\Nestedset\Contracts\NodeQueryBuilder; +use Kalnoy\Nestedset\Exceptions\NestedSetException; /** * @template Tmodel of \Illuminate\Database\Eloquent\Model @@ -17,8 +20,10 @@ * @phpstan-type NodeModel Node&Tmodel * * @extends Builder + * + * @implements NodeQueryBuilder */ -class QueryBuilder extends Builder +class QueryBuilder extends Builder implements NodeQueryBuilder { /** * @var NodeModel @@ -35,7 +40,7 @@ class QueryBuilder extends Builder * * @return array */ - public function getNodeData(mixed $id, $required = false) + public function getNodeData(mixed $id, $required = false): array { $query = $this->toBase(); @@ -61,7 +66,7 @@ public function getNodeData(mixed $id, $required = false) * * @return array */ - public function getPlainNodeData(mixed $id, $required = false) + public function getPlainNodeData(mixed $id, $required = false): array { return array_values($this->getNodeData($id, $required)); } @@ -69,9 +74,9 @@ public function getPlainNodeData(mixed $id, $required = false) /** * Scope limits query to select just root node. * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function whereIsRoot(): QueryBuilder + public function whereIsRoot(): NodeQueryBuilder { $this->query->whereNull($this->model->getParentIdName()); @@ -87,9 +92,9 @@ public function whereIsRoot(): QueryBuilder * @param bool $andSelf * @param string $boolean * - * @return QueryBuilder + * @return NodeQueryBuilder */ - public function whereAncestorOf(mixed $id, bool $andSelf = false, string $boolean = 'and') + public function whereAncestorOf(mixed $id, bool $andSelf = false, string $boolean = 'and'): NodeQueryBuilder { $keyName = $this->model->getTable() . '.' . $this->model->getKeyName(); @@ -156,9 +161,9 @@ public function whereAncestorOrSelf(mixed $id): QueryBuilder * @param NodeModel $id * @param string[] $columns * - * @return EloquentCollection + * @return NestedSetCollection */ - public function ancestorsOf(mixed $id, array $columns = ['*']) + public function ancestorsOf(mixed $id, array $columns = ['*']): NestedSetCollection { return $this->whereAncestorOf($id)->get($columns); } @@ -167,9 +172,9 @@ public function ancestorsOf(mixed $id, array $columns = ['*']) * @param NodeModel $id * @param string[] $columns * - * @return EloquentCollection + * @return NestedSetCollection */ - public function ancestorsAndSelf(mixed $id, array $columns = ['*']) + public function ancestorsAndSelf(mixed $id, array $columns = ['*']): NestedSetCollection { return $this->whereAncestorOf($id, true)->get($columns); } @@ -185,7 +190,7 @@ public function ancestorsAndSelf(mixed $id, array $columns = ['*']) * * @return QueryBuilder */ - public function whereNodeBetween(array $values, $boolean = 'and', $not = false) + public function whereNodeBetween(array $values, $boolean = 'and', $not = false): NodeQueryBuilder { $this->query->whereBetween($this->model->getTable() . '.' . $this->model->getLftName(), $values, $boolean, $not); @@ -201,7 +206,7 @@ public function whereNodeBetween(array $values, $boolean = 'and', $not = false) * * @return QueryBuilder */ - public function orWhereNodeBetween(array $values) + public function orWhereNodeBetween(array $values): NodeQueryBuilder { return $this->whereNodeBetween($values, 'or'); } @@ -211,16 +216,16 @@ public function orWhereNodeBetween(array $values) * * @since 2.0 * - * @param ?NodeModel $id - * @param string $boolean - * @param bool $not - * @param bool $andSelf + * @param ?Node $id + * @param string $boolean + * @param bool $not + * @param bool $andSelf * * @return QueryBuilder */ public function whereDescendantOf(mixed $id, $boolean = 'and', $not = false, $andSelf = false, - ) { + ): NodeQueryBuilder { if (NestedSet::isNode($id)) { $data = $id->getBounds(); } else { @@ -241,39 +246,39 @@ public function whereDescendantOf(mixed $id, $boolean = 'and', $not = false, * * @return QueryBuilder */ - public function whereNotDescendantOf(mixed $id) + public function whereNotDescendantOf(mixed $id): NodeQueryBuilder { return $this->whereDescendantOf($id, 'and', true); } /** - * @param NodeModel $id + * @param Node $id * * @return QueryBuilder */ - public function orWhereDescendantOf(mixed $id) + public function orWhereDescendantOf(mixed $id): NodeQueryBuilder { return $this->whereDescendantOf($id, 'or'); } /** - * @param NodeModel $id + * @param Node $id * * @return QueryBuilder */ - public function orWhereNotDescendantOf(mixed $id) + public function orWhereNotDescendantOf(mixed $id): NodeQueryBuilder { return $this->whereDescendantOf($id, 'or', true); } /** - * @param NodeModel $id - * @param string $boolean - * @param bool $not + * @param Node $id + * @param string $boolean + * @param bool $not * * @return QueryBuilder */ - public function whereDescendantOrSelf(mixed $id, string $boolean = 'and', bool $not = false) + public function whereDescendantOrSelf(mixed $id, string $boolean = 'and', bool $not = false): NodeQueryBuilder { return $this->whereDescendantOf($id, $boolean, $not, true); } @@ -287,9 +292,9 @@ public function whereDescendantOrSelf(mixed $id, string $boolean = 'and', bool $ * @param string[] $columns * @param bool $andSelf * - * @return EloquentCollection|Collection + * @return NestedSetCollection */ - public function descendantsOf(mixed $id, array $columns = ['*'], bool $andSelf = false) + public function descendantsOf(mixed $id, array $columns = ['*'], bool $andSelf = false): NestedSetCollection { try { return $this->whereDescendantOf($id, 'and', false, $andSelf)->get($columns); @@ -302,9 +307,9 @@ public function descendantsOf(mixed $id, array $columns = ['*'], bool $andSelf = * @param NodeModel $id * @param string[] $columns * - * @return EloquentCollection + * @return NestedSetCollection */ - public function descendantsAndSelf($id, array $columns = ['*']) + public function descendantsAndSelf($id, array $columns = ['*']): NestedSetCollection { return $this->descendantsOf($id, $columns, true); } @@ -352,7 +357,7 @@ protected function whereIsBeforeOrAfter(mixed $id, string $operator, string $boo * * @return QueryBuilder */ - public function whereIsAfter($id, $boolean = 'and') + public function whereIsAfter($id, $boolean = 'and'): NodeQueryBuilder { return $this->whereIsBeforeOrAfter($id, '>', $boolean); } @@ -367,7 +372,7 @@ public function whereIsAfter($id, $boolean = 'and') * * @return QueryBuilder */ - public function whereIsBefore($id, $boolean = 'and') + public function whereIsBefore($id, $boolean = 'and'): NodeQueryBuilder { return $this->whereIsBeforeOrAfter($id, '<', $boolean); } @@ -375,19 +380,19 @@ public function whereIsBefore($id, $boolean = 'and') /** * @return QueryBuilder */ - public function whereIsLeaf() + public function whereIsLeaf(): NodeQueryBuilder { list($lft, $rgt) = $this->wrappedColumns(); - return $this->whereRaw("$lft = $rgt - 1"); /** @phpstan-ignore-line */ + return $this->whereRaw("$lft = $rgt - 1"); } /** * @param string[] $columns * - * @return EloquentCollection + * @return NestedSetCollection */ - public function leaves(array $columns = ['*']) + public function leaves(array $columns = ['*']): NestedSetCollection { return $this->whereIsLeaf()->get($columns); } @@ -399,7 +404,7 @@ public function leaves(array $columns = ['*']) * * @return QueryBuilder */ - public function withDepth($as = 'depth') + public function withDepth($as = 'depth'): NodeQueryBuilder { if ($this->query->columns === null) { $this->query->columns = ['*']; @@ -543,7 +548,7 @@ public function reversed(): QueryBuilder * * @return int */ - public function moveNode($key, $position) + public function moveNode($key, $position): int { list($lft, $rgt) = $this->model->newNestedSetQuery() ->getPlainNodeData($key, true); @@ -595,7 +600,7 @@ public function moveNode($key, $position) * * @return int */ - public function makeGap($cut, $height) + public function makeGap($cut, $height): int { $params = compact('cut', 'height'); @@ -614,7 +619,7 @@ public function makeGap($cut, $height) * * @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): array { @@ -637,7 +642,7 @@ protected function patch(array $params): array * @param string $col * @param array{height:int,cut?:int,distance?:int,lft?:int,rgt?:int,to?:int,from?:int} $params * - * @return Expression + * @return Expression */ protected function columnPatch(string $col, array $params): Expression { @@ -828,7 +833,7 @@ protected function getMissingParentQuery() * * @return int */ - public function getTotalErrors() + public function getTotalErrors(): int { return array_sum($this->countErrors()); } @@ -840,7 +845,7 @@ public function getTotalErrors() * * @return bool */ - public function isBroken() + public function isBroken(): bool { return $this->getTotalErrors() > 0; } @@ -854,7 +859,7 @@ public function isBroken() * * @return int The number of changed nodes */ - public function fixTree($root = null) + public function fixTree($root = null): int { $columns = [ $this->model->getKeyName(), @@ -865,7 +870,7 @@ public function fixTree($root = null) $dictionary = $this->model ->newNestedSetQuery() - ->when($root, function (self $query) use ($root) { + ->when($root !== null, function (self $query) use ($root) { return $query->whereDescendantOf($root); }) ->defaultOrder() @@ -881,7 +886,7 @@ public function fixTree($root = null) * * @return int */ - public function fixSubtree($root) + public function fixSubtree($root): int { return $this->fixTree($root); } @@ -892,7 +897,7 @@ public function fixSubtree($root) * * @return int */ - protected function fixNodes(array &$dictionary, $parent = null) + protected function fixNodes(array &$dictionary, $parent = null): int { $parentId = $parent !== null ? $parent->getKey() : null; $cut = $parent !== null ? $parent->getLft() + 1 : 1; @@ -969,10 +974,10 @@ protected static function reorderNodes( * * @return int */ - public function rebuildTree(array $data, $delete = false, $root = null) + public function rebuildTree(array $data, $delete = false, $root = null): int { - if ($this->model->usesSoftDelete()) { /** @phpstan-ignore-line */ - $this->withTrashed(); /** @phpstan-ignore-line */ + if ($this->model->usesSoftDelete()) { + $this->withTrashed(); /** @phpstan-ignore method.notFound (it does exists!) */ } $existing = $this @@ -987,8 +992,8 @@ public function rebuildTree(array $data, $delete = false, $root = null) $this->buildRebuildDictionary($dictionary, $data, $existing, $parentId); - if ($existing !== null && $existing !== []) { - if ($delete && !$this->model->usesSoftDelete()) { /** @phpstan-ignore-line */ + if ($existing !== []) { + if ($delete && !$this->model->usesSoftDelete()) { $this->model ->newScopedQuery() ->whereIn($this->model->getKeyName(), array_keys($existing)) @@ -997,12 +1002,12 @@ public function rebuildTree(array $data, $delete = false, $root = null) foreach ($existing as $model) { $dictionary[$model->getParentId()][] = $model; - if ($delete && $this->model->usesSoftDelete() && /** @phpstan-ignore-line */ - !$model->{$model->getDeletedAtColumn()} /** @phpstan-ignore-line */ + if ($delete && $this->model->usesSoftDelete() && + !$model->{$model->getDeletedAtColumn()} /** @phpstan-ignore property.dynamicName */ ) { $time = $this->model->fromDateTime($this->model->freshTimestamp()); - $model->{$model->getDeletedAtColumn()} = $time; /** @phpstan-ignore-line */ + $model->{$model->getDeletedAtColumn()} = $time; /** @phpstan-ignore property.dynamicName */ } } } @@ -1018,7 +1023,7 @@ public function rebuildTree(array $data, $delete = false, $root = null) * * @return int */ - public function rebuildSubtree($root, array $data, $delete = false) + public function rebuildSubtree($root, array $data, $delete = false): int { return $this->rebuildTree($data, $delete, $root); } @@ -1077,7 +1082,7 @@ protected function buildRebuildDictionary(array &$dictionary, * * @return QueryBuilder */ - public function applyNestedSetScope($table = null) + public function applyNestedSetScope($table = null): NodeQueryBuilder { return $this->model->applyNestedSetScope($this, $table); } diff --git a/tests/models/Category.php b/tests/models/Category.php index e2217a1..e3ce3a5 100644 --- a/tests/models/Category.php +++ b/tests/models/Category.php @@ -2,19 +2,23 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use Kalnoy\Nestedset\Node; +use Kalnoy\Nestedset\Contracts\Node; use Kalnoy\Nestedset\NodeTrait; +/** + * @implements Node + */ class Category extends Model implements Node { use SoftDeletes; + /** @use NodeTrait */ use NodeTrait; protected $fillable = ['name', 'parent_id']; public $timestamps = false; - public static function resetActionsPerformed() + public static function resetActionsPerformed(): void { static::$actionsPerformed = 0; } diff --git a/tests/models/DuplicateCategory.php b/tests/models/DuplicateCategory.php index 91f2e2d..6a64503 100644 --- a/tests/models/DuplicateCategory.php +++ b/tests/models/DuplicateCategory.php @@ -1,11 +1,15 @@ + */ class DuplicateCategory extends Model implements Node { + /** @use NodeTrait */ use NodeTrait; protected $table = 'categories'; diff --git a/tests/models/MenuItem.php b/tests/models/MenuItem.php index ea17d12..cadb9fa 100644 --- a/tests/models/MenuItem.php +++ b/tests/models/MenuItem.php @@ -1,23 +1,30 @@ + */ class MenuItem extends Model implements Node { + /** @use NodeTrait */ use NodeTrait; public $timestamps = false; protected $fillable = ['menu_id', 'parent_id']; - public static function resetActionsPerformed() + public static function resetActionsPerformed(): void { static::$actionsPerformed = 0; } - protected function getScopeAttributes() + /** + * @return list + */ + protected function getScopeAttributes(): array { return ['menu_id']; }