diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 894756b..2e0933a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -27,6 +27,34 @@ jobs: name: PHP${{ matrix.php }} - Laravel${{ matrix.laravel }} + services: + mysql: + image: mysql:8 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: true + MYSQL_DATABASE: nestedset + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping --silent" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + postgres: + image: postgres:16 + env: + POSTGRES_DB: nestedset + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -35,6 +63,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + extensions: pdo_sqlite, pdo_mysql, pdo_pgsql coverage: none tools: composer:v2 @@ -43,5 +72,87 @@ jobs: composer require "illuminate/support:${{ matrix.laravel }}" "illuminate/database:${{ matrix.laravel }}" "illuminate/events:${{ matrix.laravel }}" --no-update composer update -o --quiet --prefer-dist - - name: Execute Unit Tests + - name: Tests (SQLite) + run: composer test + env: + DB_CONNECTION: testbench + + - name: Tests (MySQL) + run: composer test + env: + DB_CONNECTION: mysql + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_DATABASE: nestedset + DB_USERNAME: root + DB_PASSWORD: '' + + - name: Tests (PostgreSQL) run: composer test + env: + DB_CONNECTION: pgsql + DB_HOST: 127.0.0.1 + DB_PORT: 5432 + DB_DATABASE: nestedset + DB_USERNAME: postgres + DB_PASSWORD: password + + static-analysis: + runs-on: ubuntu-latest + + timeout-minutes: 15 + + env: + COMPOSER_NO_INTERACTION: 1 + + name: Static Analysis + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: composer update -o --quiet --prefer-dist + + - name: Run Pint + run: composer lint + + - name: Run PHPStan + run: composer analyse + + coverage: + runs-on: ubuntu-latest + + timeout-minutes: 15 + + env: + COMPOSER_NO_INTERACTION: 1 + + name: Coverage + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: pdo_sqlite + coverage: pcov + tools: composer:v2 + + - name: Install dependencies + run: composer update -o --quiet --prefer-dist + + - name: Execute tests with coverage + run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml + env: + DB_CONNECTION: testbench diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f4fb3fc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to `lunarphp/nestedset` are documented here. The format is +based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### Fixed + +- `NestedSet::isNode()` always returned `false` for genuine nodes because it + inspected object properties instead of used traits. It now uses + `class_uses_recursive()`. + +### Added + +- Native PHP type declarations across the library (PHP 8.3+). +- Laravel Pint for code style. +- Larastan static analysis (level 5) with a baseline for pre-existing findings. +- Test suite now runs against SQLite, MySQL and PostgreSQL in CI, plus a code + coverage job. +- `CONTRIBUTING.md` and this changelog. + +### Changed + +- Test suite migrated from a hand-rolled Capsule bootstrap to Orchestra + Testbench, exercising the package through a real Laravel application + (service provider registration and Blueprint macros are now covered). + +## Fork + +`lunarphp/nestedset` is a fork of +[`kalnoy/nestedset`](https://github.com/lazychaser/laravel-nestedset) by +Alexander Kalnoy, maintained for Lunar and kept current with modern Laravel +(12 and 13) and PHP 8.3+. For the history prior to the fork, see the upstream +project. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d340414 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +Thanks for contributing to `lunarphp/nestedset`. + +## Getting started + +``` +git clone https://github.com/lunarphp/nestedset +cd nestedset +composer install +``` + +## Before opening a pull request + +Run the full quality suite locally and make sure it passes: + +``` +composer lint # code style (Laravel Pint) +composer analyse # static analysis (Larastan) +composer test # test suite +``` + +`composer format` will fix most style issues automatically. + +## Tests + +The suite runs against SQLite by default. To run it against MySQL or +PostgreSQL, set the connection via environment variables — for example using +Docker: + +``` +docker run -d --rm -e POSTGRES_DB=nestedset -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=password -p 5432:5432 postgres:16 + +DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=nestedset \ + DB_USERNAME=postgres DB_PASSWORD=password composer test +``` + +The CI workflow exercises PHP 8.3/8.4, Laravel 12/13 and all three database +engines, so changes that depend on engine-specific behaviour should be covered +by a test. + +## Static analysis + +New code is analysed at PHPStan level 5. Pre-existing findings are captured in +`phpstan-baseline.neon`; please do not add to the baseline — fix new findings +instead. + +## Reporting bugs + +Open an issue with a minimal reproduction (model definition, schema and the +sequence of calls that triggers the problem). diff --git a/README.md b/README.md index afb71d9..bc1ba98 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ by Alexander Kalnoy, kept current for modern Laravel. - [Helper methods](#helper-methods) - [Checking consistency](#checking-consistency) - [Scoping](#scoping) +- [Contributing](#contributing) - [License](#license) ## What are nested sets? @@ -659,6 +660,21 @@ After [setting up your model](#the-model), fix the tree to populate `_lft` and MyModel::fixTree(); ``` +## Contributing + +Contributions are welcome. The test suite runs against SQLite, MySQL and +PostgreSQL: + +``` +composer test # run the test suite +composer lint # check code style (Laravel Pint) +composer format # fix code style +composer analyse # run static analysis (Larastan) +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for details, and +[CHANGELOG.md](CHANGELOG.md) for release notes. + ## License Released under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/composer.json b/composer.json index 2722237..e5b7e6e 100644 --- a/composer.json +++ b/composer.json @@ -31,8 +31,16 @@ "Lunar\\Nestedset\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Lunar\\Nestedset\\Tests\\": "tests/" + } + }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.0", + "laravel/pint": "^1.29", + "orchestra/testbench": "^10.0|^11.0", + "larastan/larastan": "^3.0" }, "minimum-stability": "stable", "prefer-stable": true, @@ -46,6 +54,15 @@ "scripts": { "test": [ "@php ./vendor/bin/phpunit" + ], + "format": [ + "@php ./vendor/bin/pint" + ], + "lint": [ + "@php ./vendor/bin/pint --test" + ], + "analyse": [ + "@php ./vendor/bin/phpstan analyse" ] } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..7ed81b4 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,343 @@ +parameters: + ignoreErrors: + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:isAncestorOf\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/AncestorsRelation.php + + - + message: '#^Call to method getKeyName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/AncestorsRelation.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:newScopedQuery\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/BaseRelation.php + + - + message: '#^Call to method getLftName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/BaseRelation.php + + - + message: '#^Call to method getRgtName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/BaseRelation.php + + - + message: '#^Call to method newQuery\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/BaseRelation.php + + - + message: '#^PHPDoc type Illuminate\\Database\\Eloquent\\Model\|Lunar\\Nestedset\\NodeTrait of property Lunar\\Nestedset\\BaseRelation\:\:\$parent is not covariant with PHPDoc type Illuminate\\Database\\Eloquent\\Model of overridden property Illuminate\\Database\\Eloquent\\Relations\\Relation\\:\:\$parent\.$#' + identifier: property.phpDocType + count: 1 + path: src/BaseRelation.php + + - + message: '#^Parameter \#1 \$query of method Lunar\\Nestedset\\BaseRelation\:\:addEagerConstraint\(\) expects Lunar\\Nestedset\\QueryBuilder, Illuminate\\Database\\Eloquent\\Builder\ given\.$#' + identifier: argument.type + count: 1 + path: src/BaseRelation.php + + - + message: '#^Property Lunar\\Nestedset\\BaseRelation\:\:\$parent has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: property.trait + count: 1 + path: src/BaseRelation.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:getParentIdName\(\)\.$#' + identifier: method.notFound + count: 2 + path: src/Collection.php + + - + message: '#^Call to method getKey\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/Collection.php + + - + message: '#^Call to method getLft\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/Collection.php + + - + message: '#^Call to method getParentId\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 3 + path: src/Collection.php + + - + message: '#^Call to method setRelation\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 3 + path: src/Collection.php + + - + message: '#^Empty array passed to foreach\.$#' + identifier: foreach.emptyArray + count: 2 + path: src/Collection.php + + - + message: '#^PHPDoc tag @var for variable \$child has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: varTag.trait + count: 1 + path: src/Collection.php + + - + message: '#^PHPDoc tag @var for variable \$node has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: varTag.trait + count: 3 + path: src/Collection.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 3 + path: src/Collection.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:isDescendantOf\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/DescendantsRelation.php + + - + message: '#^Trait Lunar\\Nestedset\\NodeTrait is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/NodeTrait.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:rawNode\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to an undefined method Lunar\\Nestedset\\QueryBuilder\:\:withTrashed\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method applyNestedSetScope\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 5 + path: src/QueryBuilder.php + + - + message: '#^Call to method freshTimestamp\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method fromDateTime\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method getAttributes\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method getKey\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method getKeyName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 8 + path: src/QueryBuilder.php + + - + message: '#^Call to method getLft\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method getLftName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 9 + path: src/QueryBuilder.php + + - + message: '#^Call to method getParentId\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method getParentIdName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 7 + path: src/QueryBuilder.php + + - + message: '#^Call to method getRgt\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method getRgtName\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 7 + path: src/QueryBuilder.php + + - + message: '#^Call to method getTable\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 6 + path: src/QueryBuilder.php + + - + message: '#^Call to method isDirty\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method newCollection\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method newInstance\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Call to method newNestedSetQuery\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 8 + path: src/QueryBuilder.php + + - + message: '#^Call to method newQuery\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method newScopedQuery\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 3 + path: src/QueryBuilder.php + + - + message: '#^Call to method rawNode\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 2 + path: src/QueryBuilder.php + + - + message: '#^Call to method usesSoftDelete\(\) on an unknown class Lunar\\Nestedset\\NodeTrait\.$#' + identifier: class.notFound + count: 3 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:ancestorsAndSelf\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:ancestorsOf\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:descendantsOf\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\<\(int\|string\), Illuminate\\Database\\Eloquent\\Model\>\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:descendantsOf\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:leaves\(\) should return Lunar\\Nestedset\\Collection but returns Illuminate\\Database\\Eloquent\\Collection\\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Method Lunar\\Nestedset\\QueryBuilder\:\:whereIsLeaf\(\) should return static\(Lunar\\Nestedset\\QueryBuilder\) but returns Lunar\\Nestedset\\QueryBuilder\.$#' + identifier: return.type + count: 1 + path: src/QueryBuilder.php + + - + message: '#^PHPDoc tag @var for variable \$model has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: varTag.trait + count: 3 + path: src/QueryBuilder.php + + - + message: '#^PHPDoc type Illuminate\\Database\\Eloquent\\Model\|Lunar\\Nestedset\\NodeTrait of property Lunar\\Nestedset\\QueryBuilder\:\:\$model is not covariant with PHPDoc type Illuminate\\Database\\Eloquent\\Model of overridden property Illuminate\\Database\\Eloquent\\Builder\\:\:\$model\.$#' + identifier: property.phpDocType + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Parameter \$parent of method Lunar\\Nestedset\\QueryBuilder\:\:fixNodes\(\) has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: parameter.trait + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Parameter \$root of method Lunar\\Nestedset\\QueryBuilder\:\:fixSubtree\(\) has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: parameter.trait + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Parameter \$root of method Lunar\\Nestedset\\QueryBuilder\:\:fixTree\(\) has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: parameter.trait + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Property Lunar\\Nestedset\\QueryBuilder\:\:\$model has invalid type Lunar\\Nestedset\\NodeTrait\.$#' + identifier: property.trait + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Ternary operator condition is always false\.$#' + identifier: ternary.alwaysFalse + count: 1 + path: src/QueryBuilder.php + + - + message: '#^Variable \$model in PHPDoc tag @var does not exist\.$#' + identifier: varTag.variableNotFound + count: 1 + path: src/QueryBuilder.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..b73e9a3 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +includes: + - vendor/larastan/larastan/extension.neon + - phpstan-baseline.neon + +parameters: + level: 5 + + paths: + - src diff --git a/phpunit.php b/phpunit.php deleted file mode 100644 index 7f72aff..0000000 --- a/phpunit.php +++ /dev/null @@ -1,12 +0,0 @@ -addConnection([ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'prfx_' ]); -$capsule->setEventDispatcher(new \Illuminate\Events\Dispatcher); -$capsule->bootEloquent(); -$capsule->setAsGlobal(); - -include __DIR__.'/tests/models/Category.php'; -include __DIR__.'/tests/models/MenuItem.php'; \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index fac2e3f..6dd80c7 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,10 +1,8 @@ - + - ./tests/ - ./tests/data - ./tests/models + ./tests diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..93061b6 --- /dev/null +++ b/pint.json @@ -0,0 +1,3 @@ +{ + "preset": "laravel" +} diff --git a/src/AncestorsRelation.php b/src/AncestorsRelation.php index 319af5d..a611466 100644 --- a/src/AncestorsRelation.php +++ b/src/AncestorsRelation.php @@ -8,48 +8,28 @@ class AncestorsRelation extends BaseRelation { /** * Set the base constraints on the relation query. - * - * @return void */ - public function addConstraints() + public function addConstraints(): void { - if ( ! static::$constraints) return; + if (! static::$constraints) { + return; + } $this->query->whereAncestorOf($this->parent) ->applyNestedSetScope(); } - /** - * @param Model $model - * @param $related - * - * @return bool - */ - protected function matches(Model $model, $related) + protected function matches(Model $model, Model $related): bool { return $related->isAncestorOf($model); } - /** - * @param QueryBuilder $query - * @param Model $model - * - * @return void - */ - protected function addEagerConstraint($query, $model) + protected function addEagerConstraint(QueryBuilder $query, Model $model): void { $query->orWhereAncestorOf($model); } - /** - * @param $hash - * @param $table - * @param $lft - * @param $rgt - * - * @return string - */ - protected function relationExistenceCondition($hash, $table, $lft, $rgt) + protected function relationExistenceCondition(string $hash, string $table, string $lft, string $rgt): string { $key = $this->getBaseQuery()->getGrammar()->wrap($this->parent->getKeyName()); diff --git a/src/BaseRelation.php b/src/BaseRelation.php index 8c9215c..e8b28f0 100644 --- a/src/BaseRelation.php +++ b/src/BaseRelation.php @@ -30,54 +30,24 @@ abstract class BaseRelation extends Relation /** * AncestorsRelation constructor. - * - * @param QueryBuilder $builder - * @param Model $model */ public function __construct(QueryBuilder $builder, Model $model) { - if ( ! NestedSet::isNode($model)) { + if (! NestedSet::isNode($model)) { throw new InvalidArgumentException('Model must be node.'); } parent::__construct($builder, $model); } - /** - * @param Model $model - * @param $related - * - * @return bool - */ - abstract protected function matches(Model $model, $related); + abstract protected function matches(Model $model, Model $related): bool; - /** - * @param QueryBuilder $query - * @param Model $model - * - * @return void - */ - abstract protected function addEagerConstraint($query, $model); + abstract protected function addEagerConstraint(QueryBuilder $query, Model $model): void; - /** - * @param $hash - * @param $table - * @param $lft - * @param $rgt - * - * @return string - */ - abstract protected function relationExistenceCondition($hash, $table, $lft, $rgt); + abstract protected function relationExistenceCondition(string $hash, string $table, string $lft, string $rgt): string; - /** - * @param EloquentBuilder $query - * @param EloquentBuilder $parent - * @param array $columns - * - * @return mixed - */ public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilder $parent, - $columns = [ '*' ] + $columns = ['*'] ) { $query = $this->getParent()->replicate()->newScopedQuery()->select($columns); @@ -101,12 +71,9 @@ public function getRelationExistenceQuery(EloquentBuilder $query, EloquentBuilde /** * Initialize the relation on a set of models. * - * @param array $models - * @param string $relation - * - * @return array + * @param string $relation */ - public function initRelation(array $models, $relation) + public function initRelation(array $models, $relation): array { return $models; } @@ -114,10 +81,9 @@ public function initRelation(array $models, $relation) /** * Get a relationship join table hash. * - * @param bool $incrementJoinCount - * @return string + * @param bool $incrementJoinCount */ - public function getRelationCountHash($incrementJoinCount = true) + public function getRelationCountHash($incrementJoinCount = true): string { return 'nested_set_'.($incrementJoinCount ? static::$selfJoinCount++ : static::$selfJoinCount); } @@ -134,12 +100,8 @@ public function getResults() /** * Set the constraints for an eager load of the relation. - * - * @param array $models - * - * @return void */ - public function addEagerConstraints(array $models) + public function addEagerConstraints(array $models): void { $this->query->whereNested(function (Builder $inner) use ($models) { // We will use this query in order to apply constraints to the @@ -155,13 +117,9 @@ public function addEagerConstraints(array $models) /** * Match the eagerly loaded results to their parents. * - * @param array $models - * @param EloquentCollection $results - * @param string $relation - * - * @return array + * @param string $relation */ - public function match(array $models, EloquentCollection $results, $relation) + public function match(array $models, EloquentCollection $results, $relation): array { foreach ($models as $model) { $related = $this->matchForModel($model, $results); @@ -172,13 +130,7 @@ public function match(array $models, EloquentCollection $results, $relation) return $models; } - /** - * @param Model $model - * @param EloquentCollection $results - * - * @return Collection - */ - protected function matchForModel(Model $model, EloquentCollection $results) + protected function matchForModel(Model $model, EloquentCollection $results): EloquentCollection { $result = $this->related->newCollection(); @@ -193,22 +145,18 @@ protected function matchForModel(Model $model, EloquentCollection $results) /** * Get the plain foreign key. - * - * @return mixed */ - public function getForeignKeyName() + public function getForeignKeyName(): string { // Return a stub value for relation // resolvers which need this function. return NestedSet::PARENT_ID; } - /** + /** * Get the Qualify plain foreign key. - * - * @return mixed */ - public function getQualifiedForeignKeyName() + public function getQualifiedForeignKeyName(): string { // Return a stub value for relation // resolvers which need this function. diff --git a/src/Collection.php b/src/Collection.php index 6178ddd..5df1e8b 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -11,22 +11,22 @@ class Collection extends BaseCollection * Fill `parent` and `children` relationships for every node in the collection. * * This will overwrite any previously set relations. - * - * @return $this */ - public function linkNodes() + public function linkNodes(): static { - if ($this->isEmpty()) return $this; + if ($this->isEmpty()) { + return $this; + } $groupedNodes = $this->groupBy($this->first()->getParentIdName()); /** @var NodeTrait|Model $node */ foreach ($this->items as $node) { - if ( ! $node->getParentId()) { + if (! $node->getParentId()) { $node->setRelation('parent', null); } - $children = $groupedNodes->get($node->getKey(), [ ]); + $children = $groupedNodes->get($node->getKey(), []); /** @var Model|NodeTrait $child */ foreach ($children as $child) { @@ -45,12 +45,8 @@ public function linkNodes() * To successfully build tree "id", "_lft" and "parent_id" keys must present. * * If `$root` is provided, the tree will contain only descendants of that node. - * - * @param mixed $root - * - * @return Collection */ - public function toTree($root = false) + public function toTree(mixed $root = false): static { if ($this->isEmpty()) { return new static; @@ -58,7 +54,7 @@ public function toTree($root = false) $this->linkNodes(); - $items = [ ]; + $items = []; $root = $this->getRootNodeId($root); @@ -72,12 +68,7 @@ public function toTree($root = false) return new static($items); } - /** - * @param mixed $root - * - * @return int - */ - protected function getRootNodeId($root = false) + protected function getRootNodeId(mixed $root = false): mixed { if (NestedSet::isNode($root)) { return $root->getKey(); @@ -105,16 +96,14 @@ protected function getRootNodeId($root = false) /** * Build a list of nodes that retain the order that they were pulled from * the database. - * - * @param bool $root - * - * @return static */ - public function toFlatTree($root = false) + public function toFlatTree(mixed $root = false): static { $result = new static; - if ($this->isEmpty()) return $result; + if ($this->isEmpty()) { + return $result; + } $groupedNodes = $this->groupBy($this->first()->getParentIdName()); @@ -123,13 +112,8 @@ public function toFlatTree($root = false) /** * Flatten a tree into a non recursive array. - * - * @param Collection $groupedNodes - * @param mixed $parentId - * - * @return $this */ - protected function flattenTree(self $groupedNodes, $parentId) + protected function flattenTree(self $groupedNodes, mixed $parentId): static { foreach ($groupedNodes->get($parentId, []) as $node) { $this->push($node); @@ -139,5 +123,4 @@ protected function flattenTree(self $groupedNodes, $parentId) return $this; } - -} \ No newline at end of file +} diff --git a/src/DescendantsRelation.php b/src/DescendantsRelation.php index 8b4de7e..56a5b19 100644 --- a/src/DescendantsRelation.php +++ b/src/DescendantsRelation.php @@ -2,55 +2,35 @@ namespace Lunar\Nestedset; -use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; class DescendantsRelation extends BaseRelation { - /** * Set the base constraints on the relation query. - * - * @return void */ - public function addConstraints() + public function addConstraints(): void { - if ( ! static::$constraints) return; + if (! static::$constraints) { + return; + } $this->query->whereDescendantOf($this->parent) - ->applyNestedSetScope(); + ->applyNestedSetScope(); } - /** - * @param QueryBuilder $query - * @param Model $model - */ - protected function addEagerConstraint($query, $model) + protected function addEagerConstraint(QueryBuilder $query, Model $model): void { $query->orWhereDescendantOf($model); } - /** - * @param Model $model - * @param $related - * - * @return mixed - */ - protected function matches(Model $model, $related) + protected function matches(Model $model, Model $related): bool { return $related->isDescendantOf($model); } - /** - * @param $hash - * @param $table - * @param $lft - * @param $rgt - * - * @return string - */ - protected function relationExistenceCondition($hash, $table, $lft, $rgt) + protected function relationExistenceCondition(string $hash, string $table, string $lft, string $rgt): string { return "{$hash}.{$lft} between {$table}.{$lft} + 1 and {$table}.{$rgt}"; } -} \ No newline at end of file +} diff --git a/src/NestedSet.php b/src/NestedSet.php index 3764103..45b5856 100644 --- a/src/NestedSet.php +++ b/src/NestedSet.php @@ -33,10 +33,8 @@ class NestedSet /** * Add default nested set columns to the table. Also create an index. - * - * @param \Illuminate\Database\Schema\Blueprint $table */ - public static function columns(Blueprint $table) + public static function columns(Blueprint $table): void { $table->unsignedInteger(self::LFT)->default(0); $table->unsignedInteger(self::RGT)->default(0); @@ -47,10 +45,8 @@ public static function columns(Blueprint $table) /** * Drop NestedSet columns. - * - * @param \Illuminate\Database\Schema\Blueprint $table */ - public static function dropColumns(Blueprint $table) + public static function dropColumns(Blueprint $table): void { $columns = static::getDefaultColumns(); @@ -61,23 +57,18 @@ public static function dropColumns(Blueprint $table) /** * Get a list of default columns. * - * @return array + * @return array */ - public static function getDefaultColumns() + public static function getDefaultColumns(): array { - return [ static::LFT, static::RGT, static::PARENT_ID ]; + return [static::LFT, static::RGT, static::PARENT_ID]; } /** * Replaces instanceof calls for this trait. - * - * @param mixed $node - * - * @return bool */ - public static function isNode($node) + public static function isNode(mixed $node): bool { - return is_object($node) && in_array(NodeTrait::class, (array)$node); + return is_object($node) && in_array(NodeTrait::class, class_uses_recursive($node), true); } - -} \ No newline at end of file +} diff --git a/src/NestedSetServiceProvider.php b/src/NestedSetServiceProvider.php index 247946c..32f8540 100644 --- a/src/NestedSetServiceProvider.php +++ b/src/NestedSetServiceProvider.php @@ -7,7 +7,7 @@ class NestedSetServiceProvider extends ServiceProvider { - public function register() + public function register(): void { Blueprint::macro('nestedSet', function () { NestedSet::columns($this); @@ -17,4 +17,4 @@ public function register() NestedSet::dropColumns($this); }); } -} \ No newline at end of file +} diff --git a/src/NodeTrait.php b/src/NodeTrait.php index 1f6bb5a..c72ed87 100644 --- a/src/NodeTrait.php +++ b/src/NodeTrait.php @@ -2,11 +2,13 @@ namespace Lunar\Nestedset; +use Carbon\Carbon; use Exception; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Arr; use LogicException; @@ -14,34 +16,25 @@ trait NodeTrait { /** * Pending operation. - * - * @var array */ - protected $pending; + protected ?array $pending = null; /** * Whether the node has moved since last save. - * - * @var bool */ - protected $moved = false; + protected bool $moved = false; - /** - * @var \Carbon\Carbon - */ - public static $deletedAt; + public static ?Carbon $deletedAt; /** * Keep track of the number of performed operations. - * - * @var int */ - public static $actionsPerformed = 0; + public static int $actionsPerformed = 0; /** * Sign on model events. */ - public static function bootNodeTrait() + public static function bootNodeTrait(): void { static::whenBooted(function () { static::saving(function ($model) { @@ -71,12 +64,8 @@ public static function bootNodeTrait() /** * Set an action. - * - * @param string $action - * - * @return $this */ - protected function setNodeAction($action) + protected function setNodeAction(string $action): static { $this->pending = func_get_args(); @@ -86,34 +75,33 @@ protected function setNodeAction($action) /** * Call pending action. */ - protected function callPendingAction() + protected function callPendingAction(): void { $this->moved = false; - if ( ! $this->pending && ! $this->exists) { + if (! $this->pending && ! $this->exists) { $this->makeRoot(); } - if ( ! $this->pending) return; + if (! $this->pending) { + return; + } $method = 'action'.ucfirst(array_shift($this->pending)); $parameters = $this->pending; $this->pending = null; - $this->moved = call_user_func_array([ $this, $method ], $parameters); + $this->moved = call_user_func_array([$this, $method], $parameters); } - /** - * @return bool - */ - public static function usesSoftDelete() + public static function usesSoftDelete(): bool { static $softDelete; if (is_null($softDelete)) { $softDelete = in_array( - \Illuminate\Database\Eloquent\SoftDeletes::class, + SoftDeletes::class, class_uses_recursive(static::class) ); } @@ -121,10 +109,7 @@ class_uses_recursive(static::class) return $softDelete; } - /** - * @return bool - */ - protected function actionRaw() + protected function actionRaw(): bool { return true; } @@ -132,10 +117,10 @@ protected function actionRaw() /** * Make a root node. */ - protected function actionRoot() + protected function actionRoot(): bool { // Simplest case that do not affect other nodes. - if ( ! $this->exists) { + if (! $this->exists) { $cut = $this->getLowerBound() + 1; $this->setLft($cut); @@ -149,29 +134,22 @@ protected function actionRoot() /** * Get the lower bound. - * - * @return int */ - protected function getLowerBound() + protected function getLowerBound(): int { - return (int)$this->newNestedSetQuery()->max($this->getRgtName()); + return (int) $this->newNestedSetQuery()->max($this->getRgtName()); } /** * Append or prepend a node to the parent. - * - * @param self $parent - * @param bool $prepend - * - * @return bool */ - protected function actionAppendOrPrepend(self $parent, $prepend = false) + protected function actionAppendOrPrepend(self $parent, bool $prepend = false): bool { $parent->refreshNode(); $cut = $prepend ? $parent->getLft() + 1 : $parent->getRgt(); - if ( ! $this->insertAt($cut)) { + if (! $this->insertAt($cut)) { return false; } @@ -182,12 +160,8 @@ protected function actionAppendOrPrepend(self $parent, $prepend = false) /** * Apply parent model. - * - * @param Model|null $value - * - * @return $this */ - protected function setParent($value) + protected function setParent(?Model $value): static { $this->setParentId($value ? $value->getKey() : null) ->setRelation('parent', $value); @@ -197,13 +171,8 @@ protected function setParent($value) /** * Insert node before or after another node. - * - * @param self $node - * @param bool $after - * - * @return bool */ - protected function actionBeforeOrAfter(self $node, $after = false) + protected function actionBeforeOrAfter(self $node, bool $after = false): bool { $node->refreshNode(); @@ -213,22 +182,22 @@ 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; + if (! $this->exists || static::$actionsPerformed === 0) { + return; + } $attributes = $this->newNestedSetQuery()->getNodeData($this->getKey()); $this->attributes = array_merge($this->attributes, $attributes); -// $this->original = array_merge($this->original, $attributes); + // $this->original = array_merge($this->original, $attributes); } /** * Relation to the parent. - * - * @return BelongsTo */ - public function parent() + public function parent(): BelongsTo { return $this->belongsTo(get_class($this), $this->getParentIdName()) ->setModel($this); @@ -236,10 +205,8 @@ public function parent() /** * Relation to children. - * - * @return HasMany */ - public function children() + public function children(): HasMany { return $this->hasMany(get_class($this), $this->getParentIdName()) ->setModel($this); @@ -247,20 +214,16 @@ public function children() /** * Get query for descendants of the node. - * - * @return DescendantsRelation */ - public function descendants() + public function descendants(): DescendantsRelation { return new DescendantsRelation($this->newQuery(), $this); } /** * Get query for siblings of the node. - * - * @return QueryBuilder */ - public function siblings() + public function siblings(): QueryBuilder { return $this->newScopedQuery() ->where($this->getKeyName(), '<>', $this->getKey()) @@ -269,10 +232,8 @@ public function siblings() /** * Get the node siblings and the node itself. - * - * @return \Lunar\Nestedset\QueryBuilder */ - public function siblingsAndSelf() + public function siblingsAndSelf(): QueryBuilder { return $this->newScopedQuery() ->where($this->getParentIdName(), '=', $this->getParentId()); @@ -280,22 +241,16 @@ public function siblingsAndSelf() /** * Get query for the node siblings and the node itself. - * - * @param array $columns - * - * @return \Illuminate\Database\Eloquent\Collection */ - public function getSiblingsAndSelf(array $columns = [ '*' ]) + public function getSiblingsAndSelf(array $columns = ['*']): EloquentCollection { return $this->siblingsAndSelf()->get($columns); } /** * Get query for siblings after the node. - * - * @return QueryBuilder */ - public function nextSiblings() + public function nextSiblings(): QueryBuilder { return $this->nextNodes() ->where($this->getParentIdName(), '=', $this->getParentId()); @@ -303,10 +258,8 @@ public function nextSiblings() /** * Get query for siblings before the node. - * - * @return QueryBuilder */ - public function prevSiblings() + public function prevSiblings(): QueryBuilder { return $this->prevNodes() ->where($this->getParentIdName(), '=', $this->getParentId()); @@ -314,10 +267,8 @@ public function prevSiblings() /** * Get query for nodes after current node. - * - * @return QueryBuilder */ - public function nextNodes() + public function nextNodes(): QueryBuilder { return $this->newScopedQuery() ->where($this->getLftName(), '>', $this->getLft()); @@ -325,10 +276,8 @@ public function nextNodes() /** * Get query for nodes before current node in reversed order. - * - * @return QueryBuilder */ - public function prevNodes() + public function prevNodes(): QueryBuilder { return $this->newScopedQuery() ->where($this->getLftName(), '<', $this->getLft()); @@ -336,20 +285,16 @@ public function prevNodes() /** * Get query ancestors of the node. - * - * @return AncestorsRelation */ - public function ancestors() + public function ancestors(): AncestorsRelation { return new AncestorsRelation($this->newQuery(), $this); } /** * Make this node a root node. - * - * @return $this */ - public function makeRoot() + public function makeRoot(): static { $this->setParent(null)->dirtyBounds(); @@ -358,10 +303,8 @@ public function makeRoot() /** * Save node as root. - * - * @return bool */ - public function saveAsRoot() + public function saveAsRoot(): bool { if ($this->exists && $this->isRoot()) { return $this->save(); @@ -372,59 +315,37 @@ public function saveAsRoot() /** * Append and save a node. - * - * @param self $node - * - * @return bool */ - public function appendNode(self $node) + public function appendNode(self $node): bool { return $node->appendToNode($this)->save(); } /** * Prepend and save a node. - * - * @param self $node - * - * @return bool */ - public function prependNode(self $node) + public function prependNode(self $node): bool { return $node->prependToNode($this)->save(); } /** * Append a node to the new parent. - * - * @param self $parent - * - * @return $this */ - public function appendToNode(self $parent) + public function appendToNode(self $parent): static { return $this->appendOrPrependTo($parent); } /** * Prepend a node to the new parent. - * - * @param self $parent - * - * @return $this */ - public function prependToNode(self $parent) + public function prependToNode(self $parent): static { return $this->appendOrPrependTo($parent, true); } - /** - * @param self $parent - * @param bool $prepend - * - * @return self - */ - public function appendOrPrependTo(self $parent, $prepend = false) + public function appendOrPrependTo(self $parent, bool $prepend = false): static { $this->assertNodeExists($parent) ->assertNotDescendant($parent) @@ -437,41 +358,27 @@ public function appendOrPrependTo(self $parent, $prepend = false) /** * Insert self after a node. - * - * @param self $node - * - * @return $this */ - public function afterNode(self $node) + public function afterNode(self $node): static { return $this->beforeOrAfterNode($node, true); } /** * Insert self before node. - * - * @param self $node - * - * @return $this */ - public function beforeNode(self $node) + public function beforeNode(self $node): static { return $this->beforeOrAfterNode($node); } - /** - * @param self $node - * @param bool $after - * - * @return self - */ - public function beforeOrAfterNode(self $node, $after = false) + public function beforeOrAfterNode(self $node, bool $after = false): static { $this->assertNodeExists($node) ->assertNotDescendant($node) ->assertSameScope($node); - if ( ! $this->isSiblingOf($node)) { + if (! $this->isSiblingOf($node)) { $this->setParent($node->getRelationValue('parent')); } @@ -482,26 +389,20 @@ public function beforeOrAfterNode(self $node, $after = false) /** * Insert self after a node and save. - * - * @param self $node - * - * @return bool */ - public function insertAfterNode(self $node) + public function insertAfterNode(self $node): bool { return $this->afterNode($node)->save(); } /** * Insert self before a node and save. - * - * @param self $node - * - * @return bool */ - public function insertBeforeNode(self $node) + public function insertBeforeNode(self $node): bool { - if ( ! $this->beforeNode($node)->save()) return false; + if (! $this->beforeNode($node)->save()) { + return false; + } // We'll update the target node since it will be moved $node->refreshNode(); @@ -509,14 +410,7 @@ public function insertBeforeNode(self $node) return true; } - /** - * @param $lft - * @param $rgt - * @param $parentId - * - * @return $this - */ - public function rawNode($lft, $rgt, $parentId) + public function rawNode(mixed $lft, mixed $rgt, mixed $parentId): static { $this->setLft($lft)->setRgt($rgt)->setParentId($parentId); @@ -525,52 +419,44 @@ public function rawNode($lft, $rgt, $parentId) /** * Move node up given amount of positions. - * - * @param int $amount - * - * @return bool */ - public function up($amount = 1) + public function up(int $amount = 1): bool { $sibling = $this->prevSiblings() ->defaultOrder('desc') ->skip($amount - 1) ->first(); - if ( ! $sibling) return false; + if (! $sibling) { + return false; + } return $this->insertBeforeNode($sibling); } /** * Move node down given amount of positions. - * - * @param int $amount - * - * @return bool */ - public function down($amount = 1) + public function down(int $amount = 1): bool { $sibling = $this->nextSiblings() ->defaultOrder() ->skip($amount - 1) ->first(); - if ( ! $sibling) return false; + if (! $sibling) { + return false; + } return $this->insertAfterNode($sibling); } /** * Insert node at specific position. - * - * @param int $position - * - * @return bool */ - protected function insertAt($position) + protected function insertAt(int $position): bool { - ++static::$actionsPerformed; + static::$actionsPerformed++; $result = $this->exists ? $this->moveNode($position) @@ -583,17 +469,15 @@ protected function insertAt($position) * Move a node to the new position. * * @since 2.0 - * - * @param int $position - * - * @return int */ - protected function moveNode($position) + protected function moveNode(int $position): bool { $updated = $this->newNestedSetQuery() - ->moveNode($this->getKey(), $position) > 0; + ->moveNode($this->getKey(), $position) > 0; - if ($updated) $this->refreshNode(); + if ($updated) { + $this->refreshNode(); + } return $updated; } @@ -602,12 +486,8 @@ protected function moveNode($position) * Insert new node at specified position. * * @since 2.0 - * - * @param int $position - * - * @return bool */ - protected function insertNode($position) + protected function insertNode(int $position): bool { $this->newNestedSetQuery()->makeGap($position, 2); @@ -622,7 +502,7 @@ protected function insertNode($position) /** * Update the tree when the node is removed physically. */ - protected function deleteDescendants() + protected function deleteDescendants(): void { $lft = $this->getLft(); $rgt = $this->getRgt(); @@ -647,10 +527,8 @@ protected function deleteDescendants() /** * Restore the descendants. - * - * @param $deletedAt */ - protected function restoreDescendants($deletedAt) + protected function restoreDescendants(mixed $deletedAt): void { $this->descendants() ->where($this->getDeletedAtColumn(), '>=', $deletedAt) @@ -662,7 +540,7 @@ protected function restoreDescendants($deletedAt) * * @since 2.0 */ - public function newEloquentBuilder($query) + public function newEloquentBuilder($query): QueryBuilder { return new QueryBuilder($query); } @@ -671,10 +549,8 @@ public function newEloquentBuilder($query) * Get a new base query that includes deleted nodes. * * @since 1.1 - * - * @return QueryBuilder */ - public function newNestedSetQuery($table = null) + public function newNestedSetQuery(?string $table = null): QueryBuilder { $builder = $this->usesSoftDelete() ? $this->withTrashed() @@ -683,54 +559,39 @@ public function newNestedSetQuery($table = null) return $this->applyNestedSetScope($builder, $table); } - /** - * @param string|null $table - * - * @return QueryBuilder - */ - public function newScopedQuery($table = null) + public function newScopedQuery(?string $table = null): QueryBuilder { return $this->applyNestedSetScope($this->newQuery(), $table); } /** - * @param mixed $query - * @param string|null $table - * + * @param mixed $query * @return mixed */ - public function applyNestedSetScope($query, $table = null) + public function applyNestedSetScope($query, ?string $table = null) { - if ( ! $scoped = $this->getScopeAttributes()) { + if (! $scoped = $this->getScopeAttributes()) { return $query; } - if ( ! $table) { + if (! $table) { $table = $this->getTable(); } foreach ($scoped as $attribute) { $query->where($table.'.'.$attribute, '=', - $this->getAttributeValue($attribute)); + $this->getAttributeValue($attribute)); } return $query; } - /** - * @return array - */ - protected function getScopeAttributes() + protected function getScopeAttributes(): ?array { return null; } - /** - * @param array $attributes - * - * @return QueryBuilder - */ - public static function scoped(array $attributes) + public static function scoped(array $attributes): QueryBuilder { $instance = new static; @@ -742,7 +603,7 @@ public static function scoped(array $attributes) /** * {@inheritdoc} */ - public function newCollection(array $models = array()) + public function newCollection(array $models = []): Collection { return new Collection($models); } @@ -751,10 +612,8 @@ public function newCollection(array $models = array()) * {@inheritdoc} * * Use `children` key on `$attributes` to create child nodes. - * - * @param self|null $parent */ - public static function create(array $attributes = [], ?self $parent = null) + public static function create(array $attributes = [], ?self $parent = null): static { $children = Arr::pull($attributes, 'children'); @@ -769,7 +628,7 @@ public static function create(array $attributes = [], ?self $parent = null) // Now create children $relation = new EloquentCollection; - foreach ((array)$children as $child) { + foreach ((array) $children as $child) { $relation->add($child = static::create($child, $instance)); $child->setRelation('parent', $instance); @@ -782,12 +641,12 @@ public static function create(array $attributes = [], ?self $parent = null) /** * Get node height (rgt - lft + 1). - * - * @return int */ - public function getNodeHeight() + public function getNodeHeight(): int { - if ( ! $this->exists) return 2; + if (! $this->exists) { + return 2; + } return $this->getRgt() - $this->getLft() + 1; } @@ -807,13 +666,13 @@ public function getDescendantCount() * * Behind the scenes node is appended to found parent node. * - * @param int $value - * * @throws Exception If parent node doesn't exists */ - public function setParentIdAttribute($value) + public function setParentIdAttribute(mixed $value): void { - if ($this->getParentId() == $value) return; + if ($this->getParentId() == $value) { + return; + } if ($value) { $this->appendToNode($this->newScopedQuery()->findOrFail($value)); @@ -824,78 +683,61 @@ public function setParentIdAttribute($value) /** * Get whether node is root. - * - * @return boolean */ - public function isRoot() + public function isRoot(): bool { return is_null($this->getParentId()); } - /** - * @return bool - */ - public function isLeaf() + public function isLeaf(): bool { return $this->getLft() + 1 == $this->getRgt(); } /** * Get the lft key name. - * - * @return string */ - public function getLftName() + public function getLftName(): string { return NestedSet::LFT; } /** * Get the rgt key name. - * - * @return string */ - public function getRgtName() + public function getRgtName(): string { return NestedSet::RGT; } /** * Get the parent id key name. - * - * @return string */ - public function getParentIdName() + public function getParentIdName(): string { return NestedSet::PARENT_ID; } /** * Get the value of the model's lft key. - * - * @return integer */ - public function getLft() + public function getLft(): mixed { return $this->getAttributeValue($this->getLftName()); } /** * Get the value of the model's rgt key. - * - * @return integer */ - public function getRgt() + public function getRgt(): mixed { return $this->getAttributeValue($this->getRgtName()); } /** * Get the value of the model's parent id key. - * - * @return integer */ - public function getParentId() + public function getParentId(): mixed { return $this->getAttributeValue($this->getParentIdName()); } @@ -904,12 +746,8 @@ public function getParentId() * Returns node that is next to current node without constraining to siblings. * * This can be either a next sibling or a next sibling of the parent node. - * - * @param array $columns - * - * @return self */ - public function getNextNode(array $columns = [ '*' ]) + public function getNextNode(array $columns = ['*']): ?Model { return $this->nextNodes()->defaultOrder()->first($columns); } @@ -918,94 +756,63 @@ public function getNextNode(array $columns = [ '*' ]) * Returns node that is before current node without constraining to siblings. * * This can be either a prev sibling or parent node. - * - * @param array $columns - * - * @return self */ - public function getPrevNode(array $columns = [ '*' ]) + public function getPrevNode(array $columns = ['*']): ?Model { return $this->prevNodes()->defaultOrder('desc')->first($columns); } - /** - * @param array $columns - * - * @return Collection - */ - public function getAncestors(array $columns = [ '*' ]) + public function getAncestors(array $columns = ['*']): Collection { return $this->ancestors()->get($columns); } /** - * @param array $columns - * - * @return Collection|self[] + * @return Collection */ - public function getDescendants(array $columns = [ '*' ]) + public function getDescendants(array $columns = ['*']): Collection { return $this->descendants()->get($columns); } /** - * @param array $columns - * - * @return Collection|self[] + * @return Collection */ - public function getSiblings(array $columns = [ '*' ]) + public function getSiblings(array $columns = ['*']): Collection { return $this->siblings()->get($columns); } /** - * @param array $columns - * - * @return Collection|self[] + * @return Collection */ - public function getNextSiblings(array $columns = [ '*' ]) + public function getNextSiblings(array $columns = ['*']): Collection { return $this->nextSiblings()->get($columns); } /** - * @param array $columns - * - * @return Collection|self[] + * @return Collection */ - public function getPrevSiblings(array $columns = [ '*' ]) + public function getPrevSiblings(array $columns = ['*']): Collection { return $this->prevSiblings()->get($columns); } - /** - * @param array $columns - * - * @return self - */ - public function getNextSibling(array $columns = [ '*' ]) + public function getNextSibling(array $columns = ['*']): ?Model { return $this->nextSiblings()->defaultOrder()->first($columns); } - /** - * @param array $columns - * - * @return self - */ - public function getPrevSibling(array $columns = [ '*' ]) + public function getPrevSibling(array $columns = ['*']): ?Model { return $this->prevSiblings()->defaultOrder('desc')->first($columns); } /** * Get whether a node is a descendant of other node. - * - * @param self $other - * - * @return bool */ - public function isDescendantOf(self $other) + public function isDescendantOf(self $other): bool { return $this->getLft() > $other->getLft() && $this->getLft() < $other->getRgt() && @@ -1014,12 +821,8 @@ public function isDescendantOf(self $other) /** * Get whether a node is itself or a descendant of other node. - * - * @param self $other - * - * @return bool */ - public function isSelfOrDescendantOf(self $other) + public function isSelfOrDescendantOf(self $other): bool { return $this->getLft() >= $other->getLft() && $this->getLft() < $other->getRgt(); @@ -1027,66 +830,45 @@ public function isSelfOrDescendantOf(self $other) /** * Get whether the node is immediate children of other node. - * - * @param self $other - * - * @return bool */ - public function isChildOf(self $other) + public function isChildOf(self $other): bool { return $this->getParentId() == $other->getKey(); } /** * Get whether the node is a sibling of another node. - * - * @param self $other - * - * @return bool */ - public function isSiblingOf(self $other) + public function isSiblingOf(self $other): bool { return $this->getParentId() == $other->getParentId(); } /** * Get whether the node is an ancestor of other node, including immediate parent. - * - * @param self $other - * - * @return bool */ - public function isAncestorOf(self $other) + public function isAncestorOf(self $other): bool { return $other->isDescendantOf($this); } /** * Get whether the node is itself or an ancestor of other node, including immediate parent. - * - * @param self $other - * - * @return bool */ - public function isSelfOrAncestorOf(self $other) + public function isSelfOrAncestorOf(self $other): bool { return $other->isSelfOrDescendantOf($this); } /** * Get whether the node has moved since last save. - * - * @return bool */ - public function hasMoved() + public function hasMoved(): bool { return $this->moved; } - /** - * @return array - */ - protected function getArrayableRelations() + protected function getArrayableRelations(): array { $result = parent::getArrayableRelations(); @@ -1098,62 +880,39 @@ protected function getArrayableRelations() /** * Get whether user is intended to delete the model from database entirely. - * - * @return bool */ - protected function hardDeleting() + protected function hardDeleting(): bool { return ! $this->usesSoftDelete() || $this->forceDeleting; } - /** - * @return array - */ - public function getBounds() + public function getBounds(): array { - return [ $this->getLft(), $this->getRgt() ]; + return [$this->getLft(), $this->getRgt()]; } - /** - * @param $value - * - * @return $this - */ - public function setLft($value) + public function setLft(mixed $value): static { $this->attributes[$this->getLftName()] = $value; return $this; } - /** - * @param $value - * - * @return $this - */ - public function setRgt($value) + public function setRgt(mixed $value): static { $this->attributes[$this->getRgtName()] = $value; return $this; } - /** - * @param $value - * - * @return $this - */ - public function setParentId($value) + public function setParentId(mixed $value): static { $this->attributes[$this->getParentIdName()] = $value; return $this; } - /** - * @return $this - */ - protected function dirtyBounds() + protected function dirtyBounds(): static { $this->original[$this->getLftName()] = null; $this->original[$this->getRgtName()] = null; @@ -1161,12 +920,7 @@ protected function dirtyBounds() return $this; } - /** - * @param self $node - * - * @return $this - */ - protected function assertNotDescendant(self $node) + protected function assertNotDescendant(self $node): static { if ($node == $this || $node->isDescendantOf($this)) { throw new LogicException('Node must not be a descendant.'); @@ -1175,26 +929,18 @@ protected function assertNotDescendant(self $node) return $this; } - /** - * @param self $node - * - * @return $this - */ - protected function assertNodeExists(self $node) + protected function assertNodeExists(self $node): static { - if ( ! $node->getLft() || ! $node->getRgt()) { + if (! $node->getLft() || ! $node->getRgt()) { throw new LogicException('Node must exists.'); } return $this; } - /** - * @param self $node - */ - protected function assertSameScope(self $node) + protected function assertSameScope(self $node): void { - if ( ! $scoped = $this->getScopeAttributes()) { + if (! $scoped = $this->getScopeAttributes()) { return; } @@ -1205,12 +951,9 @@ protected function assertSameScope(self $node) } } - /** - * @param self $node - */ protected function isSameScope(self $node): bool { - if ( ! $scoped = $this->getScopeAttributes()) { + if (! $scoped = $this->getScopeAttributes()) { return true; } @@ -1223,12 +966,7 @@ protected function isSameScope(self $node): bool return true; } - /** - * @param array|null $except - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function replicate(?array $except = null) + public function replicate(?array $except = null): static { $defaults = [ $this->getParentIdName(), diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 20c2980..48cea75 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -2,15 +2,14 @@ namespace Lunar\Nestedset; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Database\Query\Builder as Query; use Illuminate\Database\Query\Builder as BaseQueryBuilder; +use Illuminate\Database\Query\Builder as Query; +use Illuminate\Database\Query\Expression; use Illuminate\Support\Arr; use LogicException; -use Illuminate\Database\Query\Expression; class QueryBuilder extends Builder { @@ -23,49 +22,37 @@ class QueryBuilder extends Builder * Get node's `lft` and `rgt` values. * * @since 2.0 - * - * @param mixed $id - * @param bool $required - * - * @return array */ - public function getNodeData($id, $required = false) + public function getNodeData(mixed $id, bool $required = false): array { $query = $this->toBase(); $query->where($this->model->getKeyName(), '=', $id); - $data = $query->first([ $this->model->getLftName(), - $this->model->getRgtName() ]); + $data = $query->first([$this->model->getLftName(), + $this->model->getRgtName()]); - if ( ! $data && $required) { + if (! $data && $required) { throw new ModelNotFoundException; } - return (array)$data; + return (array) $data; } /** * Get plain node data. * * @since 2.0 - * - * @param mixed $id - * @param bool $required - * - * @return array */ - public function getPlainNodeData($id, $required = false) + public function getPlainNodeData(mixed $id, bool $required = false): array { return array_values($this->getNodeData($id, $required)); } /** * Scope limits query to select just root node. - * - * @return $this */ - public function whereIsRoot() + public function whereIsRoot(): static { $this->query->whereNull($this->model->getParentIdName()); @@ -76,17 +63,10 @@ public function whereIsRoot() * Limit results to ancestors of specified node. * * @since 2.0 - * - * @param mixed $id - * @param bool $andSelf - * - * @param string $boolean - * - * @return $this */ - public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') + public function whereAncestorOf(mixed $id, bool $andSelf = false, string $boolean = 'and'): static { - $keyName = $this->model->getTable() . '.' . $this->model->getKeyName(); + $keyName = $this->model->getTable().'.'.$this->model->getKeyName(); $model = null; if (NestedSet::isNode($id)) { @@ -100,7 +80,7 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') $valueQuery = $this->model ->newQuery() ->toBase() - ->select("_.".$this->model->getRgtName()) + ->select('_.'.$this->model->getRgtName()) ->from($this->model->getTable().' as _') ->where($this->model->getKeyName(), '=', $id) ->limit(1); @@ -111,12 +91,12 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') } $this->query->whereNested(function ($inner) use ($model, $value, $andSelf, $id, $keyName) { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $wrappedTable = $this->query->getGrammar()->wrapTable($this->model->getTable()); $inner->whereRaw("{$value} between {$wrappedTable}.{$lft} and {$wrappedTable}.{$rgt}"); - if ( ! $andSelf) { + if (! $andSelf) { $inner->where($keyName, '<>', $id); } if ($model !== null) { @@ -129,23 +109,12 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') return $this; } - /** - * @param $id - * @param bool $andSelf - * - * @return $this - */ - public function orWhereAncestorOf($id, $andSelf = false) + public function orWhereAncestorOf(mixed $id, bool $andSelf = false): static { return $this->whereAncestorOf($id, $andSelf, 'or'); } - /** - * @param $id - * - * @return QueryBuilder - */ - public function whereAncestorOrSelf($id) + public function whereAncestorOrSelf(mixed $id): static { return $this->whereAncestorOf($id, true); } @@ -154,24 +123,13 @@ public function whereAncestorOrSelf($id) * Get ancestors of specified node. * * @since 2.0 - * - * @param mixed $id - * @param array $columns - * - * @return \Lunar\Nestedset\Collection */ - public function ancestorsOf($id, array $columns = array( '*' )) + public function ancestorsOf(mixed $id, array $columns = ['*']): Collection { return $this->whereAncestorOf($id)->get($columns); } - /** - * @param $id - * @param array $columns - * - * @return \Lunar\Nestedset\Collection - */ - public function ancestorsAndSelf($id, array $columns = [ '*' ]) + public function ancestorsAndSelf(mixed $id, array $columns = ['*']): Collection { return $this->whereAncestorOf($id, true)->get($columns); } @@ -180,17 +138,10 @@ public function ancestorsAndSelf($id, array $columns = [ '*' ]) * Add node selection statement between specified range. * * @since 2.0 - * - * @param array $values - * @param string $boolean - * @param bool $not - * @param Query $query - * - * @return $this */ - public function whereNodeBetween($values, $boolean = 'and', $not = false, $query = null) + public function whereNodeBetween(array $values, string $boolean = 'and', bool $not = false, ?Query $query = null): static { - ($query ?? $this->query)->whereBetween($this->model->getTable() . '.' . $this->model->getLftName(), $values, $boolean, $not); + ($query ?? $this->query)->whereBetween($this->model->getTable().'.'.$this->model->getLftName(), $values, $boolean, $not); return $this; } @@ -199,12 +150,8 @@ public function whereNodeBetween($values, $boolean = 'and', $not = false, $query * Add node selection statement between specified range joined with `or` operator. * * @since 2.0 - * - * @param array $values - * - * @return $this */ - public function orWhereNodeBetween($values) + public function orWhereNodeBetween(array $values): static { return $this->whereNodeBetween($values, 'or'); } @@ -213,17 +160,10 @@ public function orWhereNodeBetween($values) * Add constraint statement to descendants of specified node. * * @since 2.0 - * - * @param mixed $id - * @param string $boolean - * @param bool $not - * @param bool $andSelf - * - * @return $this */ - public function whereDescendantOf($id, $boolean = 'and', $not = false, - $andSelf = false - ) { + public function whereDescendantOf(mixed $id, string $boolean = 'and', bool $not = false, + bool $andSelf = false + ): static { $this->query->whereNested(function (Query $inner) use ($id, $andSelf, $not) { if (NestedSet::isNode($id)) { $id->applyNestedSetScope($inner); @@ -236,8 +176,8 @@ public function whereDescendantOf($id, $boolean = 'and', $not = false, } // Don't include the node - if (!$andSelf) { - ++$data[0]; + if (! $andSelf) { + $data[0]++; } return $this->whereNodeBetween($data, 'and', $not, $inner); @@ -246,44 +186,22 @@ public function whereDescendantOf($id, $boolean = 'and', $not = false, return $this; } - /** - * @param mixed $id - * - * @return QueryBuilder - */ - public function whereNotDescendantOf($id) + public function whereNotDescendantOf(mixed $id): static { return $this->whereDescendantOf($id, 'and', true); } - /** - * @param mixed $id - * - * @return QueryBuilder - */ - public function orWhereDescendantOf($id) + public function orWhereDescendantOf(mixed $id): static { return $this->whereDescendantOf($id, 'or'); } - /** - * @param mixed $id - * - * @return QueryBuilder - */ - public function orWhereNotDescendantOf($id) + public function orWhereNotDescendantOf(mixed $id): static { return $this->whereDescendantOf($id, 'or', true); } - /** - * @param $id - * @param string $boolean - * @param bool $not - * - * @return $this - */ - public function whereDescendantOrSelf($id, $boolean = 'and', $not = false) + public function whereDescendantOrSelf(mixed $id, string $boolean = 'and', bool $not = false): static { return $this->whereDescendantOf($id, $boolean, $not, true); } @@ -292,43 +210,22 @@ public function whereDescendantOrSelf($id, $boolean = 'and', $not = false) * Get descendants of specified node. * * @since 2.0 - * - * @param mixed $id - * @param array $columns - * @param bool $andSelf - * - * @return Collection */ - public function descendantsOf($id, array $columns = [ '*' ], $andSelf = false) + public function descendantsOf(mixed $id, array $columns = ['*'], bool $andSelf = false): Collection { try { return $this->whereDescendantOf($id, 'and', false, $andSelf)->get($columns); - } - - catch (ModelNotFoundException $e) { + } catch (ModelNotFoundException $e) { return $this->model->newCollection(); } } - /** - * @param $id - * @param array $columns - * - * @return Collection - */ - public function descendantsAndSelf($id, array $columns = [ '*' ]) + public function descendantsAndSelf(mixed $id, array $columns = ['*']): Collection { return $this->descendantsOf($id, $columns, true); } - /** - * @param $id - * @param $operator - * @param $boolean - * - * @return $this - */ - protected function whereIsBeforeOrAfter($id, $operator, $boolean) + protected function whereIsBeforeOrAfter(mixed $id, string $operator, string $boolean): static { if (NestedSet::isNode($id)) { $value = '?'; @@ -347,9 +244,9 @@ protected function whereIsBeforeOrAfter($id, $operator, $boolean) $value = '('.$valueQuery->toSql().')'; } - list($lft,) = $this->wrappedColumns(); + [$lft] = $this->wrappedColumns(); - $this->query->whereRaw("{$lft} {$operator} {$value}", [ ], $boolean); + $this->query->whereRaw("{$lft} {$operator} {$value}", [], $boolean); return $this; } @@ -358,13 +255,8 @@ protected function whereIsBeforeOrAfter($id, $operator, $boolean) * Constraint nodes to those that are after specified node. * * @since 2.0 - * - * @param mixed $id - * @param string $boolean - * - * @return $this */ - public function whereIsAfter($id, $boolean = 'and') + public function whereIsAfter(mixed $id, string $boolean = 'and'): static { return $this->whereIsBeforeOrAfter($id, '>', $boolean); } @@ -373,51 +265,36 @@ public function whereIsAfter($id, $boolean = 'and') * Constraint nodes to those that are before specified node. * * @since 2.0 - * - * @param mixed $id - * @param string $boolean - * - * @return $this */ - public function whereIsBefore($id, $boolean = 'and') + public function whereIsBefore(mixed $id, string $boolean = 'and'): static { return $this->whereIsBeforeOrAfter($id, '<', $boolean); } - /** - * @return $this - */ - public function whereIsLeaf() + public function whereIsLeaf(): static { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); return $this->whereRaw("$lft = $rgt - 1"); } - /** - * @param array $columns - * - * @return Collection - */ - public function leaves(array $columns = [ '*']) + public function leaves(array $columns = ['*']): Collection { return $this->whereIsLeaf()->get($columns); } /** * Include depth level into the result. - * - * @param string $as - * - * @return $this */ - public function withDepth($as = 'depth') + public function withDepth(string $as = 'depth'): static { - if ($this->query->columns === null) $this->query->columns = [ '*' ]; + if ($this->query->columns === null) { + $this->query->columns = ['*']; + } $table = $this->wrappedTable(); - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $alias = '_d'; $wrappedAlias = $this->query->getGrammar()->wrapTable($alias); @@ -438,10 +315,8 @@ public function withDepth($as = 'depth') * Get wrapped `lft` and `rgt` column names. * * @since 2.0 - * - * @return array */ - protected function wrappedColumns() + protected function wrappedColumns(): array { $grammar = $this->query->getGrammar(); @@ -455,10 +330,8 @@ protected function wrappedColumns() * Get a wrapped table name. * * @since 2.0 - * - * @return string */ - protected function wrappedTable() + protected function wrappedTable(): string { return $this->query->getGrammar()->wrapTable($this->getQuery()->from); } @@ -467,20 +340,16 @@ protected function wrappedTable() * Wrap model's key name. * * @since 2.0 - * - * @return string */ - protected function wrappedKey() + protected function wrappedKey(): string { return $this->query->getGrammar()->wrap($this->model->getKeyName()); } /** * Exclude root node from the result. - * - * @return $this */ - public function withoutRoot() + public function withoutRoot(): static { $this->query->whereNotNull($this->model->getParentIdName()); @@ -492,10 +361,8 @@ public function withoutRoot() * * @since 2.0 * @deprecated since v4.1 - * - * @return $this */ - public function hasParent() + public function hasParent(): static { $this->query->whereNotNull($this->model->getParentIdName()); @@ -507,12 +374,10 @@ public function hasParent() * * @since 2.0 * @deprecated since v4.1 - * - * @return $this */ - public function hasChildren() + public function hasChildren(): static { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $this->query->whereRaw("{$rgt} > {$lft} + 1"); @@ -521,12 +386,8 @@ public function hasChildren() /** * Order by node position. - * - * @param string $dir - * - * @return $this */ - public function defaultOrder($dir = 'asc') + public function defaultOrder(string $dir = 'asc'): static { $this->query->orders = null; @@ -537,26 +398,19 @@ public function defaultOrder($dir = 'asc') /** * Order by reversed node position. - * - * @return $this */ - public function reversed() + public function reversed(): static { return $this->defaultOrder('desc'); } /** * Move a node to the new position. - * - * @param mixed $key - * @param int $position - * - * @return int */ - public function moveNode($key, $position) + public function moveNode(mixed $key, int $position): int { - list($lft, $rgt) = $this->model->newNestedSetQuery() - ->getPlainNodeData($key, true); + [$lft, $rgt] = $this->model->newNestedSetQuery() + ->getPlainNodeData($key, true); if ($lft < $position && $position <= $rgt) { throw new LogicException('Cannot move node into itself.'); @@ -585,7 +439,7 @@ public function moveNode($key, $position) $params = compact('lft', 'rgt', 'from', 'to', 'height', 'distance'); - $boundary = [ $from, $to ]; + $boundary = [$from, $to]; $query = $this->toBase()->where(function (Query $inner) use ($boundary) { $inner->whereBetween($this->model->getLftName(), $boundary); @@ -599,13 +453,8 @@ public function moveNode($key, $position) * Make or remove gap in the tree. Negative height will remove gap. * * @since 2.0 - * - * @param int $cut - * @param int $height - * - * @return int */ - public function makeGap($cut, $height) + public function makeGap(int $cut, int $height): int { $params = compact('cut', 'height'); @@ -621,18 +470,14 @@ public function makeGap($cut, $height) * Get patch for columns. * * @since 2.0 - * - * @param array $params - * - * @return array */ - protected function patch(array $params) + protected function patch(array $params): array { $grammar = $this->query->getGrammar(); $columns = []; - foreach ([ $this->model->getLftName(), $this->model->getRgtName() ] as $col) { + foreach ([$this->model->getLftName(), $this->model->getRgtName()] as $col) { $columns[$col] = $this->columnPatch($grammar->wrap($col), $params); } @@ -643,18 +488,15 @@ protected function patch(array $params) * Get patch for single column. * * @since 2.0 - * - * @param string $col - * @param array $params - * - * @return string */ - protected function columnPatch($col, array $params) + protected function columnPatch(string $col, array $params): Expression { extract($params); /** @var int $height */ - if ($height > 0) $height = '+'.$height; + if ($height > 0) { + $height = '+'.$height; + } if (isset($cut)) { return new Expression("case when {$col} >= {$cut} then {$col}{$height} else {$col} end"); @@ -665,9 +507,11 @@ protected function columnPatch($col, array $params) /** @var int $rgt */ /** @var int $from */ /** @var int $to */ - if ($distance > 0) $distance = '+'.$distance; + if ($distance > 0) { + $distance = '+'.$distance; + } - return new Expression("case ". + return new Expression('case '. "when {$col} between {$lft} and {$rgt} then {$col}{$distance} ". // Move the node "when {$col} between {$from} and {$to} then {$col}{$height} ". // Move other nodes "else {$col} end" @@ -678,10 +522,8 @@ protected function columnPatch($col, array $params) * Get statistics of errors of the tree. * * @since 2.0 - * - * @return array */ - public function countErrors() + public function countErrors(): array { $checks = []; @@ -695,7 +537,7 @@ public function countErrors() $checks['wrong_parent'] = $this->getWrongParentQuery(); // Check for nodes that have missing parent - $checks['missing_parent' ] = $this->getMissingParentQuery(); + $checks['missing_parent'] = $this->getMissingParentQuery(); $query = $this->query->newQuery(); @@ -705,29 +547,23 @@ public function countErrors() $query->selectSub($inner, $key); } - return (array)$query->first(); + return (array) $query->first(); } - /** - * @return BaseQueryBuilder - */ - protected function getOdnessQuery() + protected function getOdnessQuery(): BaseQueryBuilder { return $this->model ->newNestedSetQuery() ->toBase() ->whereNested(function (BaseQueryBuilder $inner) { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $inner->whereRaw("{$lft} >= {$rgt}") - ->orWhereRaw("({$rgt} - {$lft}) % 2 = 0"); + ->orWhereRaw("({$rgt} - {$lft}) % 2 = 0"); }); } - /** - * @return BaseQueryBuilder - */ - protected function getDuplicatesQuery() + protected function getDuplicatesQuery(): BaseQueryBuilder { $table = $this->wrappedTable(); $keyName = $this->wrappedKey(); @@ -744,21 +580,18 @@ protected function getDuplicatesQuery() ->from($this->query->raw("{$table} as {$waFirst}, {$table} {$waSecond}")) ->whereRaw("{$waFirst}.{$keyName} < {$waSecond}.{$keyName}") ->whereNested(function (BaseQueryBuilder $inner) use ($waFirst, $waSecond) { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $inner->orWhereRaw("{$waFirst}.{$lft}={$waSecond}.{$lft}") - ->orWhereRaw("{$waFirst}.{$rgt}={$waSecond}.{$rgt}") - ->orWhereRaw("{$waFirst}.{$lft}={$waSecond}.{$rgt}") - ->orWhereRaw("{$waFirst}.{$rgt}={$waSecond}.{$lft}"); + ->orWhereRaw("{$waFirst}.{$rgt}={$waSecond}.{$rgt}") + ->orWhereRaw("{$waFirst}.{$lft}={$waSecond}.{$rgt}") + ->orWhereRaw("{$waFirst}.{$rgt}={$waSecond}.{$lft}"); }); return $this->model->applyNestedSetScope($query, $secondAlias); } - /** - * @return BaseQueryBuilder - */ - protected function getWrongParentQuery() + protected function getWrongParentQuery(): BaseQueryBuilder { $table = $this->wrappedTable(); $keyName = $this->wrappedKey(); @@ -783,11 +616,11 @@ protected function getWrongParentQuery() ->whereRaw("{$waInterm}.{$keyName} <> {$waParent}.{$keyName}") ->whereRaw("{$waInterm}.{$keyName} <> {$waChild}.{$keyName}") ->whereNested(function (BaseQueryBuilder $inner) use ($waInterm, $waChild, $waParent) { - list($lft, $rgt) = $this->wrappedColumns(); + [$lft, $rgt] = $this->wrappedColumns(); $inner->whereRaw("{$waChild}.{$lft} not between {$waParent}.{$lft} and {$waParent}.{$rgt}") - ->orWhereRaw("{$waChild}.{$lft} between {$waInterm}.{$lft} and {$waInterm}.{$rgt}") - ->whereRaw("{$waInterm}.{$lft} between {$waParent}.{$lft} and {$waParent}.{$rgt}"); + ->orWhereRaw("{$waChild}.{$lft} between {$waInterm}.{$lft} and {$waInterm}.{$rgt}") + ->whereRaw("{$waInterm}.{$lft} between {$waParent}.{$lft} and {$waParent}.{$rgt}"); }); $this->model->applyNestedSetScope($query, $parentAlias); @@ -796,10 +629,7 @@ protected function getWrongParentQuery() return $query; } - /** - * @return $this - */ - protected function getMissingParentQuery() + protected function getMissingParentQuery(): BaseQueryBuilder { return $this->model ->newNestedSetQuery() @@ -824,7 +654,7 @@ protected function getMissingParentQuery() $this->model->applyNestedSetScope($existsCheck, $alias); $inner->whereRaw("{$parentIdName} is not null") - ->addWhereExistsQuery($existsCheck, 'and', true); + ->addWhereExistsQuery($existsCheck, 'and', true); }); } @@ -832,10 +662,8 @@ protected function getMissingParentQuery() * Get the number of total errors of the tree. * * @since 2.0 - * - * @return int */ - public function getTotalErrors() + public function getTotalErrors(): int { return array_sum($this->countErrors()); } @@ -844,10 +672,8 @@ public function getTotalErrors() * Get whether the tree is broken. * * @since 2.0 - * - * @return bool */ - public function isBroken() + public function isBroken(): bool { return $this->getTotalErrors() > 0; } @@ -857,11 +683,10 @@ public function isBroken() * * Nodes with invalid parent are saved as roots. * - * @param null|NodeTrait|Model $root - * + * @param null|NodeTrait|Model $root * @return int The number of changed nodes */ - public function fixTree($root = null) + public function fixTree($root = null): int { $columns = [ $this->model->getKeyName(), @@ -884,22 +709,17 @@ public function fixTree($root = null) } /** - * @param NodeTrait|Model $root - * - * @return int + * @param NodeTrait|Model $root */ - public function fixSubtree($root) + public function fixSubtree($root): int { return $this->fixTree($root); } /** - * @param array $dictionary - * @param NodeTrait|Model|null $parent - * - * @return int + * @param NodeTrait|Model|null $parent */ - protected function fixNodes(array &$dictionary, $parent = null) + protected function fixNodes(array &$dictionary, $parent = null): int { $parentId = $parent ? $parent->getKey() : null; $cut = $parent ? $parent->getLft() + 1 : 1; @@ -910,7 +730,7 @@ protected function fixNodes(array &$dictionary, $parent = null) $cut = self::reorderNodes($dictionary, $updated, $parentId, $cut); // Save nodes that have invalid parent as roots - while ( ! empty($dictionary)) { + while (! empty($dictionary)) { $dictionary[null] = reset($dictionary); unset($dictionary[key($dictionary)]); @@ -932,18 +752,12 @@ protected function fixNodes(array &$dictionary, $parent = null) } /** - * @param array $dictionary - * @param array $updated - * @param $parentId - * @param int $cut - * - * @return int * @internal param int $fixed */ protected static function reorderNodes( - array &$dictionary, array &$updated, $parentId = null, $cut = 1 - ) { - if ( ! isset($dictionary[$parentId])) { + array &$dictionary, array &$updated, mixed $parentId = null, int $cut = 1 + ): int { + if (! isset($dictionary[$parentId])) { return $cut; } @@ -957,7 +771,7 @@ protected static function reorderNodes( $updated[] = $model; } - ++$cut; + $cut++; } unset($dictionary[$parentId]); @@ -970,14 +784,11 @@ protected static function reorderNodes( * * If item data does not contain primary key, new node will be created. * - * @param array $data - * @param bool $delete Whether to delete nodes that exists but not in the data - * array - * @param null $root - * - * @return int + * @param bool $delete Whether to delete nodes that exists but not in the data + * array + * @param null $root */ - public function rebuildTree(array $data, $delete = false, $root = null) + public function rebuildTree(array $data, bool $delete = false, $root = null): int { if ($this->model->usesSoftDelete()) { $this->withTrashed(); @@ -996,7 +807,7 @@ public function rebuildTree(array $data, $delete = false, $root = null) $this->buildRebuildDictionary($dictionary, $data, $existing, $parentId); /** @var Model|NodeTrait $model */ - if ( ! empty($existing)) { + if (! empty($existing)) { if ($delete && ! $this->model->usesSoftDelete()) { $this->model ->newScopedQuery() @@ -1020,41 +831,27 @@ public function rebuildTree(array $data, $delete = false, $root = null) return $this->fixNodes($dictionary, $root); } - /** - * @param $root - * @param array $data - * @param bool $delete - * - * @return int - */ - public function rebuildSubtree($root, array $data, $delete = false) + public function rebuildSubtree($root, array $data, bool $delete = false): int { return $this->rebuildTree($data, $delete, $root); } - /** - * @param array $dictionary - * @param array $data - * @param array $existing - * @param mixed $parentId - */ protected function buildRebuildDictionary(array &$dictionary, - array $data, - array &$existing, - $parentId = null - ) { + array $data, + array &$existing, + mixed $parentId = null + ): void { $keyName = $this->model->getKeyName(); foreach ($data as $itemData) { /** @var NodeTrait|Model $model */ - - if ( ! isset($itemData[$keyName])) { + if (! isset($itemData[$keyName])) { $model = $this->model->newInstance($this->model->getAttributes()); // Set some values that will be fixed later $model->rawNode(0, 0, $parentId); } else { - if ( ! isset($existing[$key = $itemData[$keyName]])) { + if (! isset($existing[$key = $itemData[$keyName]])) { throw new ModelNotFoundException; } @@ -1070,33 +867,26 @@ protected function buildRebuildDictionary(array &$dictionary, $dictionary[$parentId][] = $model; - if ( ! isset($itemData['children'])) continue; + if (! isset($itemData['children'])) { + continue; + } $this->buildRebuildDictionary($dictionary, - $itemData['children'], - $existing, - $model->getKey()); + $itemData['children'], + $existing, + $model->getKey()); } } - /** - * @param string|null $table - * - * @return $this - */ - public function applyNestedSetScope($table = null) + public function applyNestedSetScope(?string $table = null): static { return $this->model->applyNestedSetScope($this, $table); } /** * Get the root node. - * - * @param array $columns - * - * @return self */ - public function root(array $columns = ['*']) + public function root(array $columns = ['*']): ?Model { return $this->whereIsRoot()->first($columns); } diff --git a/tests/Models/Category.php b/tests/Models/Category.php new file mode 100644 index 0000000..e426ff9 --- /dev/null +++ b/tests/Models/Category.php @@ -0,0 +1,21 @@ +dropIfExists('categories'); + Schema::dropIfExists('categories'); - Capsule::disableQueryLog(); - - $schema->create('categories', function (\Illuminate\Database\Schema\Blueprint $table) { + Schema::create('categories', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->softDeletes(); NestedSet::columns($table); }); - Capsule::enableQueryLog(); - } - - public function setUp(): void - { $data = include __DIR__.'/data/categories.php'; - Capsule::table('categories')->insert($data); + DB::table('categories')->insert($data); - Capsule::flushQueryLog(); + $this->resetAutoIncrement('categories'); + + DB::connection()->enableQueryLog(); + DB::connection()->flushQueryLog(); Category::resetActionsPerformed(); date_default_timezone_set('America/Denver'); } - public function tearDown(): void - { - Capsule::table('categories')->truncate(); - } - - // public static function tearDownAfterClass() - // { - // $log = Capsule::getQueryLog(); - // foreach ($log as $item) { - // echo $item['query']." with ".implode(', ', $item['bindings'])."\n"; - // } - // } - public function assertTreeNotBroken($table = 'categories') { - $checks = array(); + $checks = []; - $connection = Capsule::connection(); + $connection = DB::connection(); $table = $connection->getQueryGrammar()->wrapTable($table); @@ -62,11 +54,11 @@ public function assertTreeNotBroken($table = 'categories') // Check if lft and rgt values are unique $checks[] = "from $table c1, $table c2 where c1.id <> c2.id and ". - "(c1._lft=c2._lft or c1._rgt=c2._rgt or c1._lft=c2._rgt or c1._rgt=c2._lft)"; + '(c1._lft=c2._lft or c1._rgt=c2._rgt or c1._lft=c2._rgt or c1._rgt=c2._lft)'; // Check if parent_id is set correctly $checks[] = "from $table c, $table p, $table m where c.parent_id=p.id and m.id <> p.id and m.id <> c.id and ". - "(c._lft not between p._lft and p._rgt or c._lft between m._lft and m._rgt and m._lft between p._lft and p._rgt)"; + '(c._lft not between p._lft and p._rgt or c._lft between m._lft and m._rgt and m._lft between p._lft and p._rgt)'; foreach ($checks as $i => $check) { $checks[$i] = 'select 1 as error '.$check; @@ -77,17 +69,19 @@ public function assertTreeNotBroken($table = 'categories') $actual = $connection->selectOne($sql); $this->assertEquals(null, $actual->errors, "The tree structure of $table is broken!"); - $actual = (array)Capsule::connection()->selectOne($sql); + $actual = (array) DB::connection()->selectOne($sql); - $this->assertEquals(array('errors' => null), $actual, "The tree structure of $table is broken!"); + $this->assertEquals(['errors' => null], $actual, "The tree structure of $table is broken!"); } public function dumpTree($items = null) { - if ( ! $items) $items = Category::withTrashed()->defaultOrder()->get(); + if (! $items) { + $items = Category::withTrashed()->defaultOrder()->get(); + } foreach ($items as $item) { - echo PHP_EOL.($item->trashed() ? '-' : '+').' '.$item->name." ".$item->getKey().' '.$item->getLft()." ".$item->getRgt().' '.$item->getParentId(); + echo PHP_EOL.($item->trashed() ? '-' : '+').' '.$item->name.' '.$item->getKey().' '.$item->getLft().' '.$item->getRgt().' '.$item->getParentId(); } } @@ -98,16 +92,14 @@ public function assertNodeReceivesValidValues($node) $nodeInDb = $this->findCategory($node->name); $this->assertEquals( - [ $nodeInDb->getLft(), $nodeInDb->getRgt() ], - [ $lft, $rgt ], + [$nodeInDb->getLft(), $nodeInDb->getRgt()], + [$lft, $rgt], 'Node is not synced with database after save.' ); } /** - * @param $name - * - * @return \Category + * @return Category */ public function findCategory($name, $withTrashed = false) { @@ -118,7 +110,7 @@ public function findCategory($name, $withTrashed = false) return $q->whereName($name)->first(); } - public function testTreeNotBroken() + public function test_tree_not_broken() { $this->assertTreeNotBroken(); $this->assertFalse(Category::isBroken()); @@ -126,29 +118,29 @@ public function testTreeNotBroken() public function nodeValues($node) { - return array($node->_lft, $node->_rgt, $node->parent_id); + return [$node->_lft, $node->_rgt, $node->parent_id]; } - public function testGetsNodeData() + public function test_gets_node_data() { $data = Category::getNodeData(3); - $this->assertEquals([ '_lft' => 3, '_rgt' => 4 ], $data); + $this->assertEquals(['_lft' => 3, '_rgt' => 4], $data); } - public function testGetsPlainNodeData() + public function test_gets_plain_node_data() { $data = Category::getPlainNodeData(3); - $this->assertEquals([ 3, 4 ], $data); + $this->assertEquals([3, 4], $data); } - public function testReceivesValidValuesWhenAppendedTo() + public function test_receives_valid_values_when_appended_to() { - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $root = Category::root(); - $accepted = array($root->_rgt, $root->_rgt + 1, $root->id); + $accepted = [$root->_rgt, $root->_rgt + 1, $root->id]; $root->appendNode($node); @@ -159,45 +151,45 @@ public function testReceivesValidValuesWhenAppendedTo() $this->assertTrue($node->isDescendantOf($root)); } - public function testReceivesValidValuesWhenPrependedTo() + public function test_receives_valid_values_when_prepended_to() { $root = Category::root(); - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $root->prependNode($node); $this->assertTrue($node->hasMoved()); - $this->assertEquals(array($root->_lft + 1, $root->_lft + 2, $root->id), $this->nodeValues($node)); + $this->assertEquals([$root->_lft + 1, $root->_lft + 2, $root->id], $this->nodeValues($node)); $this->assertTreeNotBroken(); $this->assertTrue($node->isDescendantOf($root)); $this->assertTrue($root->isAncestorOf($node)); $this->assertTrue($node->isChildOf($root)); } - public function testReceivesValidValuesWhenInsertedAfter() + public function test_receives_valid_values_when_inserted_after() { $target = $this->findCategory('apple'); - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $node->afterNode($target)->save(); $this->assertTrue($node->hasMoved()); - $this->assertEquals(array($target->_rgt + 1, $target->_rgt + 2, $target->parent->id), $this->nodeValues($node)); + $this->assertEquals([$target->_rgt + 1, $target->_rgt + 2, $target->parent->id], $this->nodeValues($node)); $this->assertTreeNotBroken(); $this->assertFalse($node->isDirty()); $this->assertTrue($node->isSiblingOf($target)); } - public function testReceivesValidValuesWhenInsertedBefore() + public function test_receives_valid_values_when_inserted_before() { $target = $this->findCategory('apple'); - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $node->beforeNode($target)->save(); $this->assertTrue($node->hasMoved()); - $this->assertEquals(array($target->_lft, $target->_lft + 1, $target->parent->id), $this->nodeValues($node)); + $this->assertEquals([$target->_lft, $target->_lft + 1, $target->parent->id], $this->nodeValues($node)); $this->assertTreeNotBroken(); } - public function testCategoryMovesDown() + public function test_category_moves_down() { $node = $this->findCategory('apple'); $target = $this->findCategory('mobile'); @@ -209,7 +201,7 @@ public function testCategoryMovesDown() $this->assertTreeNotBroken(); } - public function testCategoryMovesUp() + public function test_category_moves_up() { $node = $this->findCategory('samsung'); $target = $this->findCategory('notebooks'); @@ -221,7 +213,7 @@ public function testCategoryMovesUp() $this->assertNodeReceivesValidValues($node); } - public function testFailsToInsertIntoChild() + public function test_fails_to_insert_into_child() { $this->expectException(Exception::class); @@ -231,7 +223,7 @@ public function testFailsToInsertIntoChild() $node->afterNode($target)->save(); } - public function testFailsToAppendIntoItself() + public function test_fails_to_append_into_itself() { $this->expectException(Exception::class); @@ -240,7 +232,7 @@ public function testFailsToAppendIntoItself() $node->appendToNode($node)->save(); } - public function testFailsToPrependIntoItself() + public function test_fails_to_prepend_into_itself() { $this->expectException(Exception::class); @@ -249,40 +241,40 @@ public function testFailsToPrependIntoItself() $node->prependTo($node)->save(); } - public function testWithoutRootWorks() + public function test_without_root_works() { $result = Category::withoutRoot()->pluck('name'); $this->assertNotEquals('store', $result); } - public function testAncestorsReturnsAncestorsWithoutNodeItself() + public function test_ancestors_returns_ancestors_without_node_itself() { $node = $this->findCategory('apple'); $path = all($node->ancestors()->pluck('name')); - $this->assertEquals(array('store', 'notebooks'), $path); + $this->assertEquals(['store', 'notebooks'], $path); } - public function testGetsAncestorsByStatic() + public function test_gets_ancestors_by_static() { $path = all(Category::ancestorsOf(3)->pluck('name')); - $this->assertEquals(array('store', 'notebooks'), $path); + $this->assertEquals(['store', 'notebooks'], $path); } - public function testGetsAncestorsDirect() + public function test_gets_ancestors_direct() { $path = all(Category::find(8)->getAncestors()->pluck('id')); - $this->assertEquals(array(1, 5, 7), $path); + $this->assertEquals([1, 5, 7], $path); } - public function testDescendants() + public function test_descendants() { $node = $this->findCategory('mobile'); $descendants = all($node->descendants()->pluck('name')); - $expected = array('nokia', 'samsung', 'galaxy', 'sony', 'lenovo'); + $expected = ['nokia', 'samsung', 'galaxy', 'sony', 'lenovo']; $this->assertEquals($expected, $descendants); @@ -292,35 +284,35 @@ public function testDescendants() $this->assertEquals($expected, $descendants); $descendants = all(Category::descendantsAndSelf(7)->pluck('name')); - $expected = [ 'samsung', 'galaxy' ]; + $expected = ['samsung', 'galaxy']; $this->assertEquals($expected, $descendants); } - public function testWithDepthWorks() + public function test_with_depth_works() { $nodes = all(Category::withDepth()->limit(4)->pluck('depth')); - $this->assertEquals(array(0, 1, 2, 2), $nodes); + $this->assertEquals([0, 1, 2, 2], $nodes); } - public function testWithDepthWithCustomKeyWorks() + public function test_with_depth_with_custom_key_works() { $node = Category::whereIsRoot()->withDepth('level')->first(); $this->assertTrue(isset($node['level'])); } - public function testWithDepthWorksAlongWithDefaultKeys() + public function test_with_depth_works_along_with_default_keys() { $node = Category::withDepth()->first(); $this->assertTrue(isset($node->name)); } - public function testParentIdAttributeAccessorAppendsNode() + public function test_parent_id_attribute_accessor_appends_node() { - $node = new Category(array('name' => 'lg', 'parent_id' => 5)); + $node = new Category(['name' => 'lg', 'parent_id' => 5]); $node->save(); $this->assertEquals(5, $node->parent_id); @@ -335,7 +327,7 @@ public function testParentIdAttributeAccessorAppendsNode() $this->assertTrue($node->isRoot()); } - public function testFailsToSaveNodeUntilNotInserted() + public function test_fails_to_save_node_until_not_inserted() { $this->expectException(Exception::class); @@ -343,21 +335,21 @@ public function testFailsToSaveNodeUntilNotInserted() $node->save(); } - public function testNodeIsDeletedWithDescendants() + public function test_node_is_deleted_with_descendants() { $node = $this->findCategory('mobile'); $node->forceDelete(); $this->assertTreeNotBroken(); - $nodes = Category::whereIn('id', array(5, 6, 7, 8, 9))->count(); + $nodes = Category::whereIn('id', [5, 6, 7, 8, 9])->count(); $this->assertEquals(0, $nodes); $root = Category::root(); $this->assertEquals(8, $root->getRgt()); } - public function testNodeIsSoftDeleted() + public function test_node_is_soft_deleted() { $root = Category::root(); @@ -373,7 +365,7 @@ public function testNodeIsSoftDeleted() $node = $this->findCategory('mobile'); $node->delete(); - $nodes = Category::whereIn('id', array(5, 6, 7, 8, 9))->count(); + $nodes = Category::whereIn('id', [5, 6, 7, 8, 9])->count(); $this->assertEquals(0, $nodes); $originalRgt = $root->getRgt(); @@ -389,7 +381,7 @@ public function testNodeIsSoftDeleted() $this->assertNotNull($this->findCategory('nokia')); } - public function testSoftDeletedNodeisDeletedWhenParentIsDeleted() + public function test_soft_deleted_nodeis_deleted_when_parent_is_deleted() { $this->findCategory('samsung')->delete(); @@ -401,34 +393,34 @@ public function testSoftDeletedNodeisDeletedWhenParentIsDeleted() $this->assertNull($this->findCategory('sony')); } - public function testFailsToSaveNodeUntilParentIsSaved() + public function test_fails_to_save_node_until_parent_is_saved() { $this->expectException(Exception::class); - $node = new Category(array('title' => 'Node')); - $parent = new Category(array('title' => 'Parent')); + $node = new Category(['title' => 'Node']); + $parent = new Category(['title' => 'Parent']); $node->appendTo($parent)->save(); } - public function testSiblings() + public function test_siblings() { $node = $this->findCategory('samsung'); $siblings = all($node->siblings()->pluck('id')); $next = all($node->nextSiblings()->pluck('id')); $prev = all($node->prevSiblings()->pluck('id')); - $this->assertEquals(array(6, 9, 10), $siblings); - $this->assertEquals(array(9, 10), $next); - $this->assertEquals(array(6), $prev); + $this->assertEquals([6, 9, 10], $siblings); + $this->assertEquals([9, 10], $next); + $this->assertEquals([6], $prev); $siblings = all($node->getSiblings()->pluck('id')); $next = all($node->getNextSiblings()->pluck('id')); $prev = all($node->getPrevSiblings()->pluck('id')); - $this->assertEquals(array(6, 9, 10), $siblings); - $this->assertEquals(array(9, 10), $next); - $this->assertEquals(array(6), $prev); + $this->assertEquals([6, 9, 10], $siblings); + $this->assertEquals([9, 10], $next); + $this->assertEquals([6], $prev); $next = $node->getNextSibling(); $prev = $node->getPrevSibling(); @@ -437,7 +429,7 @@ public function testSiblings() $this->assertEquals(6, $prev->id); } - public function testFetchesReversed() + public function test_fetches_reversed() { $node = $this->findCategory('sony'); $siblings = $node->prevSiblings()->reversed()->value('id'); @@ -445,9 +437,9 @@ public function testFetchesReversed() $this->assertEquals(7, $siblings); } - public function testToTreeBuildsWithDefaultOrder() + public function test_to_tree_builds_with_default_order() { - $tree = Category::whereBetween('_lft', array(8, 17))->defaultOrder()->get()->toTree(); + $tree = Category::whereBetween('_lft', [8, 17])->defaultOrder()->get()->toTree(); $this->assertEquals(1, count($tree)); @@ -456,10 +448,10 @@ public function testToTreeBuildsWithDefaultOrder() $this->assertEquals(4, count($root->children)); } - public function testToTreeBuildsWithCustomOrder() + public function test_to_tree_builds_with_custom_order() { - $tree = Category::whereBetween('_lft', array(8, 17)) - ->orderBy('title') + $tree = Category::whereBetween('_lft', [8, 17]) + ->orderBy('name') ->get() ->toTree(); @@ -471,28 +463,28 @@ public function testToTreeBuildsWithCustomOrder() $this->assertEquals($root, $root->children->first()->parent); } - public function testToTreeWithSpecifiedRoot() + public function test_to_tree_with_specified_root() { $node = $this->findCategory('mobile'); - $nodes = Category::whereBetween('_lft', array(8, 17))->get(); + $nodes = Category::whereBetween('_lft', [8, 17])->get(); - $tree1 = \Lunar\Nestedset\Collection::make($nodes)->toTree(5); - $tree2 = \Lunar\Nestedset\Collection::make($nodes)->toTree($node); + $tree1 = Collection::make($nodes)->toTree(5); + $tree2 = Collection::make($nodes)->toTree($node); $this->assertEquals(4, $tree1->count()); $this->assertEquals(4, $tree2->count()); } - public function testToTreeBuildsWithDefaultOrderAndMultipleRootNodes() + public function test_to_tree_builds_with_default_order_and_multiple_root_nodes() { $tree = Category::withoutRoot()->get()->toTree(); $this->assertEquals(2, count($tree)); } - public function testToTreeBuildsWithRootItemIdProvided() + public function test_to_tree_builds_with_root_item_id_provided() { - $tree = Category::whereBetween('_lft', array(8, 17))->get()->toTree(5); + $tree = Category::whereBetween('_lft', [8, 17])->get()->toTree(5); $this->assertEquals(4, count($tree)); @@ -501,7 +493,7 @@ public function testToTreeBuildsWithRootItemIdProvided() $this->assertEquals(1, count($root->children)); } - public function testRetrievesNextNode() + public function test_retrieves_next_node() { $node = $this->findCategory('apple'); $next = $node->nextNodes()->first(); @@ -509,7 +501,7 @@ public function testRetrievesNextNode() $this->assertEquals('lenovo', $next->name); } - public function testRetrievesPrevNode() + public function test_retrieves_prev_node() { $node = $this->findCategory('apple'); $next = $node->getPrevNode(); @@ -517,24 +509,24 @@ public function testRetrievesPrevNode() $this->assertEquals('notebooks', $next->name); } - public function testMultipleAppendageWorks() + public function test_multiple_appendage_works() { $parent = $this->findCategory('mobile'); - $child = new Category([ 'name' => 'test' ]); + $child = new Category(['name' => 'test']); $parent->appendNode($child); - $child->appendNode(new Category([ 'name' => 'sub' ])); + $child->appendNode(new Category(['name' => 'sub'])); - $parent->appendNode(new Category([ 'name' => 'test2' ])); + $parent->appendNode(new Category(['name' => 'test2'])); $this->assertTreeNotBroken(); } - public function testDefaultCategoryIsSavedAsRoot() + public function test_default_category_is_saved_as_root() { - $node = new Category([ 'name' => 'test' ]); + $node = new Category(['name' => 'test']); $node->save(); $this->assertEquals(23, $node->_lft); @@ -543,7 +535,7 @@ public function testDefaultCategoryIsSavedAsRoot() $this->assertTrue($node->isRoot()); } - public function testExistingCategorySavedAsRoot() + public function test_existing_category_saved_as_root() { $node = $this->findCategory('apple'); $node->saveAsRoot(); @@ -552,7 +544,7 @@ public function testExistingCategorySavedAsRoot() $this->assertTrue($node->isRoot()); } - public function testNodeMovesDownSeveralPositions() + public function test_node_moves_down_several_positions() { $node = $this->findCategory('nokia'); @@ -561,7 +553,7 @@ public function testNodeMovesDownSeveralPositions() $this->assertEquals($node->_lft, 15); } - public function testNodeMovesUpSeveralPositions() + public function test_node_moves_up_several_positions() { $node = $this->findCategory('sony'); @@ -570,19 +562,19 @@ public function testNodeMovesUpSeveralPositions() $this->assertEquals($node->_lft, 9); } - public function testCountsTreeErrors() + public function test_counts_tree_errors() { $errors = Category::countErrors(); - $this->assertEquals([ 'oddness' => 0, - 'duplicates' => 0, - 'wrong_parent' => 0, - 'missing_parent' => 0 ], $errors); + $this->assertEquals(['oddness' => 0, + 'duplicates' => 0, + 'wrong_parent' => 0, + 'missing_parent' => 0], $errors); - Category::where('id', '=', 5)->update([ '_lft' => 14 ]); - Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]); - Category::where('id', '=', 11)->update([ '_lft' => 20 ]); - Category::where('id', '=', 4)->update([ 'parent_id' => 24 ]); + Category::where('id', '=', 5)->update(['_lft' => 14]); + Category::where('id', '=', 8)->update(['parent_id' => 2]); + Category::where('id', '=', 11)->update(['_lft' => 20]); + Category::where('id', '=', 4)->update(['parent_id' => 24]); $errors = Category::countErrors(); @@ -591,33 +583,32 @@ public function testCountsTreeErrors() $this->assertEquals(1, $errors['missing_parent']); } - public function testCreatesNode() + public function test_creates_node() { - $node = Category::create([ 'name' => 'test' ]); + $node = Category::create(['name' => 'test']); $this->assertEquals(23, $node->getLft()); } - public function testCreatesViaRelationship() + public function test_creates_via_relationship() { $node = $this->findCategory('apple'); - $child = $node->children()->create([ 'name' => 'test' ]); + $child = $node->children()->create(['name' => 'test']); $this->assertTreeNotBroken(); } - public function testCreatesTree() + public function test_creates_tree() { $node = Category::create( - [ - 'name' => 'test', - 'children' => [ - [ 'name' => 'test2' ], - [ 'name' => 'test3' ], - ], - ]); + 'name' => 'test', + 'children' => [ + ['name' => 'test2'], + ['name' => 'test3'], + ], + ]); $this->assertTreeNotBroken(); @@ -629,54 +620,53 @@ public function testCreatesTree() $this->assertEquals('test2', $node->children[0]->name); } - public function testDescendantsOfNonExistingNode() + public function test_descendants_of_non_existing_node() { $node = new Category; $this->assertTrue($node->getDescendants()->isEmpty()); } - public function testWhereDescendantsOf() + public function test_where_descendants_of() { - $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + $this->expectException(ModelNotFoundException::class); Category::whereDescendantOf(124)->get(); } - public function testAncestorsByNode() + public function test_ancestors_by_node() { $category = $this->findCategory('apple'); $ancestors = all(Category::whereAncestorOf($category)->pluck('id')); - $this->assertEquals([ 1, 2 ], $ancestors); + $this->assertEquals([1, 2], $ancestors); } - public function testDescendantsByNode() + public function test_descendants_by_node() { $category = $this->findCategory('notebooks'); $res = all(Category::whereDescendantOf($category)->pluck('id')); - $this->assertEquals([ 3, 4 ], $res); + $this->assertEquals([3, 4], $res); } - public function testMultipleDeletionsDoNotBrakeTree() + public function test_multiple_deletions_do_not_brake_tree() { $category = $this->findCategory('mobile'); - foreach ($category->children()->take(2)->get() as $child) - { + foreach ($category->children()->take(2)->get() as $child) { $child->forceDelete(); } $this->assertTreeNotBroken(); } - public function testTreeIsFixed() + public function test_tree_is_fixed() { - Category::where('id', '=', 5)->update([ '_lft' => 14 ]); - Category::where('id', '=', 8)->update([ 'parent_id' => 2 ]); - Category::where('id', '=', 11)->update([ '_lft' => 20 ]); - Category::where('id', '=', 2)->update([ 'parent_id' => 24 ]); + Category::where('id', '=', 5)->update(['_lft' => 14]); + Category::where('id', '=', 8)->update(['parent_id' => 2]); + Category::where('id', '=', 11)->update(['_lft' => 20]); + Category::where('id', '=', 2)->update(['parent_id' => 24]); $fixed = Category::fixTree(); @@ -692,9 +682,9 @@ public function testTreeIsFixed() $this->assertEquals(null, $node->getParentId()); } - public function testSubtreeIsFixed() + public function test_subtree_is_fixed() { - Category::where('id', '=', 8)->update([ '_lft' => 11 ]); + Category::where('id', '=', 8)->update(['_lft' => 11]); $fixed = Category::fixSubtree(Category::find(5)); $this->assertEquals($fixed, 1); @@ -702,7 +692,7 @@ public function testSubtreeIsFixed() $this->assertEquals(Category::find(8)->getLft(), 12); } - public function testParentIdDirtiness() + public function test_parent_id_dirtiness() { $node = $this->findCategory('apple'); $node->parent_id = 5; @@ -715,7 +705,7 @@ public function testParentIdDirtiness() $this->assertTrue($node->isDirty('parent_id')); } - public function testIsDirtyMovement() + public function test_is_dirty_movement() { $node = $this->findCategory('apple'); $otherNode = $this->findCategory('samsung'); @@ -736,7 +726,7 @@ public function testIsDirtyMovement() $this->assertTrue($node->isDirty()); } - public function testRootNodesMoving() + public function test_root_nodes_moving() { $node = $this->findCategory('store'); $node->down(); @@ -744,7 +734,7 @@ public function testRootNodesMoving() $this->assertEquals(3, $node->getLft()); } - public function testDescendantsRelation() + public function test_descendants_relation() { $node = $this->findCategory('notebooks'); $result = $node->descendants; @@ -753,9 +743,9 @@ public function testDescendantsRelation() $this->assertEquals('apple', $result->first()->name); } - public function testDescendantsEagerlyLoaded() + public function test_descendants_eagerly_loaded() { - $nodes = Category::whereIn('id', [ 2, 5 ])->get(); + $nodes = Category::whereIn('id', [2, 5])->get(); $nodes->load('descendants'); @@ -763,9 +753,9 @@ public function testDescendantsEagerlyLoaded() $this->assertTrue($nodes->first()->relationLoaded('descendants')); } - public function testDescendantsRelationQuery() + public function test_descendants_relation_query() { - $nodes = Category::has('descendants')->whereIn('id', [ 2, 3 ])->get(); + $nodes = Category::has('descendants')->whereIn('id', [2, 3])->get(); $this->assertEquals(1, $nodes->count()); $this->assertEquals(2, $nodes->first()->getKey()); @@ -777,26 +767,26 @@ public function testDescendantsRelationQuery() $this->assertEquals(5, $nodes[1]->getKey()); } - public function testParentRelationQuery() + public function test_parent_relation_query() { - $nodes = Category::has('parent')->whereIn('id', [ 1, 2 ]); + $nodes = Category::has('parent')->whereIn('id', [1, 2]); $this->assertEquals(1, $nodes->count()); $this->assertEquals(2, $nodes->first()->getKey()); } - public function testRebuildTree() + public function test_rebuild_tree() { $fixed = Category::rebuildTree([ [ 'id' => 1, 'children' => [ - [ 'id' => 10 ], - [ 'id' => 3, 'name' => 'apple v2', 'children' => [ [ 'name' => 'new node' ] ] ], - [ 'id' => 2 ], + ['id' => 10], + ['id' => 3, 'name' => 'apple v2', 'children' => [['name' => 'new node']]], + ['id' => 2], - ] - ] + ], + ], ]); $this->assertTrue($fixed > 0); @@ -814,11 +804,11 @@ public function testRebuildTree() $this->assertEquals(3, $node->getParentId()); } - public function testRebuildSubtree() + public function test_rebuild_subtree() { $fixed = Category::rebuildSubtree(Category::find(7), [ - [ 'name' => 'new node' ], - [ 'id' => '8' ], + ['name' => 'new node'], + ['id' => '8'], ]); $this->assertTrue($fixed > 0); @@ -830,9 +820,9 @@ public function testRebuildSubtree() $this->assertEquals($node->getLft(), 12); } - public function testRebuildTreeWithDeletion() + public function test_rebuild_tree_with_deletion() { - Category::rebuildTree([ [ 'name' => 'all deleted' ] ], true); + Category::rebuildTree([['name' => 'all deleted']], true); $this->assertTreeNotBroken(); @@ -846,14 +836,14 @@ public function testRebuildTreeWithDeletion() $this->assertTrue($nodes->count() > 1); } - public function testRebuildFailsWithInvalidPK() + public function test_rebuild_fails_with_invalid_pk() { - $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + $this->expectException(ModelNotFoundException::class); - Category::rebuildTree([ [ 'id' => 24 ] ]); + Category::rebuildTree([['id' => 24]]); } - public function testFlatTree() + public function test_flat_tree() { $node = $this->findCategory('mobile'); $tree = $node->descendants()->orderBy('name')->get()->toFlatTree(); @@ -880,7 +870,7 @@ public function testFlatTree() $duplicate->saveAsRoot(); }*/ - public function testWhereIsLeaf() + public function test_where_is_leaf() { $categories = Category::leaves(); @@ -893,12 +883,12 @@ public function testWhereIsLeaf() $this->assertFalse($category->isLeaf()); } - public function testEagerLoadAncestors() + public function test_eager_load_ancestors() { - $queryLogCount = count(Capsule::connection()->getQueryLog()); + $queryLogCount = count(DB::connection()->getQueryLog()); $categories = Category::with('ancestors')->orderBy('name')->get(); - $this->assertEquals($queryLogCount + 2, count(Capsule::connection()->getQueryLog())); + $this->assertEquals($queryLogCount + 2, count(DB::connection()->getQueryLog())); $expectedShape = [ 'apple (3)}' => 'store (1) > notebooks (2)', @@ -911,26 +901,28 @@ public function testEagerLoadAncestors() 'samsung (7)}' => 'store (1) > mobile (5)', 'sony (9)}' => 'store (1) > mobile (5)', 'store (1)}' => '', - 'store_2 (11)}' => '' + 'store_2 (11)}' => '', ]; $output = []; foreach ($categories as $category) { $output["{$category->name} ({$category->id})}"] = $category->ancestors->count() - ? implode(' > ', $category->ancestors->map(function ($cat) { return "{$cat->name} ({$cat->id})"; })->toArray()) + ? implode(' > ', $category->ancestors->map(function ($cat) { + return "{$cat->name} ({$cat->id})"; + })->toArray()) : ''; } $this->assertEquals($expectedShape, $output); } - public function testLazyLoadAncestors() + public function test_lazy_load_ancestors() { - $queryLogCount = count(Capsule::connection()->getQueryLog()); + $queryLogCount = count(DB::connection()->getQueryLog()); $categories = Category::orderBy('name')->get(); - $this->assertEquals($queryLogCount + 1, count(Capsule::connection()->getQueryLog())); + $this->assertEquals($queryLogCount + 1, count(DB::connection()->getQueryLog())); $expectedShape = [ 'apple (3)}' => 'store (1) > notebooks (2)', @@ -943,37 +935,39 @@ public function testLazyLoadAncestors() 'samsung (7)}' => 'store (1) > mobile (5)', 'sony (9)}' => 'store (1) > mobile (5)', 'store (1)}' => '', - 'store_2 (11)}' => '' + 'store_2 (11)}' => '', ]; $output = []; foreach ($categories as $category) { $output["{$category->name} ({$category->id})}"] = $category->ancestors->count() - ? implode(' > ', $category->ancestors->map(function ($cat) { return "{$cat->name} ({$cat->id})"; })->toArray()) + ? implode(' > ', $category->ancestors->map(function ($cat) { + return "{$cat->name} ({$cat->id})"; + })->toArray()) : ''; } // assert that there is number of original query + 1 + number of rows to fulfill the relation - $this->assertEquals($queryLogCount + 12, count(Capsule::connection()->getQueryLog())); + $this->assertEquals($queryLogCount + 12, count(DB::connection()->getQueryLog())); $this->assertEquals($expectedShape, $output); } - public function testWhereHasCountQueryForAncestors() + public function test_where_has_count_query_for_ancestors() { $categories = all(Category::has('ancestors', '>', 2)->pluck('name')); - $this->assertEquals([ 'galaxy' ], $categories); + $this->assertEquals(['galaxy'], $categories); $categories = all(Category::whereHas('ancestors', function ($query) { $query->where('id', 5); })->pluck('name')); - $this->assertEquals([ 'nokia', 'samsung', 'galaxy', 'sony', 'lenovo' ], $categories); + $this->assertEquals(['nokia', 'samsung', 'galaxy', 'sony', 'lenovo'], $categories); } - public function testReplication() + public function test_replication() { $category = $this->findCategory('nokia'); $category = $category->replicate(); @@ -992,6 +986,12 @@ public function testReplication() $this->assertEquals(1, $category->getParentId()); } + public function test_is_node_detects_trait_users() + { + $this->assertTrue(NestedSet::isNode(new Category)); + $this->assertFalse(NestedSet::isNode(new stdClass)); + $this->assertFalse(NestedSet::isNode('not an object')); + } } function all($items) diff --git a/tests/ScopedNodeTest.php b/tests/ScopedNodeTest.php index 2da34b9..500caa9 100644 --- a/tests/ScopedNodeTest.php +++ b/tests/ScopedNodeTest.php @@ -1,58 +1,56 @@ dropIfExists('menu_items'); + parent::setUp(); - Capsule::disableQueryLog(); + Schema::dropIfExists('menu_items'); - $schema->create('menu_items', function (\Illuminate\Database\Schema\Blueprint $table) { + Schema::create('menu_items', function (Blueprint $table) { $table->increments('id'); $table->unsignedInteger('menu_id'); $table->string('title')->nullable(); NestedSet::columns($table); }); - Capsule::enableQueryLog(); - } - - public function setUp(): void - { $data = include __DIR__.'/data/menu_items.php'; - Capsule::table('menu_items')->insert($data); + DB::table('menu_items')->insert($data); - Capsule::flushQueryLog(); + $this->resetAutoIncrement('menu_items'); + + DB::connection()->enableQueryLog(); + DB::connection()->flushQueryLog(); MenuItem::resetActionsPerformed(); date_default_timezone_set('America/Denver'); } - public function tearDown(): void - { - Capsule::table('menu_items')->truncate(); - } - public function assertTreeNotBroken($menuId) { - $this->assertFalse(MenuItem::scoped([ 'menu_id' => $menuId ])->isBroken()); + $this->assertFalse(MenuItem::scoped(['menu_id' => $menuId])->isBroken()); } - public function testNotBroken() + public function test_not_broken() { $this->assertTreeNotBroken(1); $this->assertTreeNotBroken(2); } - public function testMovingNodeNotAffectingOtherMenu() + public function test_moving_node_not_affecting_other_menu() { $node = MenuItem::where('menu_id', '=', 1)->first(); @@ -63,14 +61,14 @@ public function testMovingNodeNotAffectingOtherMenu() $this->assertEquals(1, $node->getLft()); } - public function testScoped() + public function test_scoped() { - $node = MenuItem::scoped([ 'menu_id' => 2 ])->first(); + $node = MenuItem::scoped(['menu_id' => 2])->first(); $this->assertEquals(3, $node->getKey()); } - public function testSiblings() + public function test_siblings() { $node = MenuItem::find(1); @@ -90,7 +88,7 @@ public function testSiblings() $this->assertEquals(1, $result->first()->getKey()); } - public function testDescendants() + public function test_descendants() { $node = MenuItem::find(2); @@ -99,7 +97,7 @@ public function testDescendants() $this->assertEquals(1, $result->count()); $this->assertEquals(5, $result->first()->getKey()); - $node = MenuItem::scoped([ 'menu_id' => 1 ])->with('descendants')->find(2); + $node = MenuItem::scoped(['menu_id' => 1])->with('descendants')->find(2); $result = $node->descendants; @@ -107,7 +105,7 @@ public function testDescendants() $this->assertEquals(5, $result->first()->getKey()); } - public function testAncestors() + public function test_ancestors() { $node = MenuItem::find(5); @@ -116,7 +114,7 @@ public function testAncestors() $this->assertEquals(1, $result->count()); $this->assertEquals(2, $result->first()->getKey()); - $node = MenuItem::scoped([ 'menu_id' => 1 ])->with('ancestors')->find(5); + $node = MenuItem::scoped(['menu_id' => 1])->with('ancestors')->find(5); $result = $node->ancestors; @@ -124,9 +122,9 @@ public function testAncestors() $this->assertEquals(2, $result->first()->getKey()); } - public function testDepth() + public function test_depth() { - $node = MenuItem::scoped([ 'menu_id' => 1 ])->withDepth()->where('id', '=', 5)->first(); + $node = MenuItem::scoped(['menu_id' => 1])->withDepth()->where('id', '=', 5)->first(); $this->assertEquals(1, $node->depth); @@ -137,7 +135,7 @@ public function testDepth() $this->assertEquals(1, $result->first()->depth); } - public function testSaveAsRoot() + public function test_save_as_root() { $node = MenuItem::find(5); @@ -149,9 +147,9 @@ public function testSaveAsRoot() $this->assertOtherScopeNotAffected(); } - public function testInsertion() + public function test_insertion() { - $node = MenuItem::create([ 'menu_id' => 1, 'parent_id' => 5 ]); + $node = MenuItem::create(['menu_id' => 1, 'parent_id' => 5]); $this->assertEquals(5, $node->parent_id); $this->assertEquals(5, $node->getLft()); @@ -159,14 +157,14 @@ public function testInsertion() $this->assertOtherScopeNotAffected(); } - public function testInsertionToParentFromOtherScope() + public function test_insertion_to_parent_from_other_scope() { - $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + $this->expectException(ModelNotFoundException::class); - $node = MenuItem::create([ 'menu_id' => 2, 'parent_id' => 5 ]); + $node = MenuItem::create(['menu_id' => 2, 'parent_id' => 5]); } - public function testDeletion() + public function test_deletion() { $node = MenuItem::find(2)->delete(); @@ -177,7 +175,7 @@ public function testDeletion() $this->assertOtherScopeNotAffected(); } - public function testMoving() + public function test_moving() { $node = MenuItem::find(1); $this->assertTrue($node->down()); @@ -200,7 +198,7 @@ protected function assertOtherScopeNotAffected() MenuItem::scoped([ 'menu_id' => 2 ])->rebuildTree($data); }*/ - public function testAppendingToAnotherScopeFails() + public function test_appending_to_another_scope_fails() { $this->expectException(LogicException::class); @@ -210,7 +208,7 @@ public function testAppendingToAnotherScopeFails() $a->appendToNode($b)->save(); } - public function testInsertingBeforeAnotherScopeFails() + public function test_inserting_before_another_scope_fails() { $this->expectException(LogicException::class); @@ -220,7 +218,7 @@ public function testInsertingBeforeAnotherScopeFails() $a->insertAfterNode($b); } - public function testEagerLoadingAncestorsWithScope() + public function test_eager_loading_ancestors_with_scope() { $filteredNodes = MenuItem::where('title', 'menu item 3')->with(['ancestors'])->get(); @@ -228,11 +226,11 @@ public function testEagerLoadingAncestorsWithScope() $this->assertEquals(4, $filteredNodes->find(6)->ancestors[0]->id); } - public function testEagerLoadingDescendantsWithScope() + public function test_eager_loading_descendants_with_scope() { $filteredNodes = MenuItem::where('title', 'menu item 2')->with(['descendants'])->get(); $this->assertEquals(5, $filteredNodes->find(2)->descendants[0]->id); $this->assertEquals(6, $filteredNodes->find(4)->descendants[0]->id); } -} \ No newline at end of file +} diff --git a/tests/ServiceProviderTest.php b/tests/ServiceProviderTest.php new file mode 100644 index 0000000..ef4419b --- /dev/null +++ b/tests/ServiceProviderTest.php @@ -0,0 +1,58 @@ +increments('id'); + $table->nestedSet(); + }); + + foreach (NestedSet::getDefaultColumns() as $column) { + $this->assertTrue( + Schema::hasColumn('macro_nodes', $column), + "Expected column [$column] to be created by the nestedSet() macro." + ); + } + } + + public function test_drop_nested_set_blueprint_macro_removes_columns() + { + Schema::create('macro_nodes', function (Blueprint $table) { + $table->increments('id'); + $table->nestedSet(); + }); + + Schema::table('macro_nodes', function (Blueprint $table) { + $table->dropNestedSet(); + }); + + foreach (NestedSet::getDefaultColumns() as $column) { + $this->assertFalse( + Schema::hasColumn('macro_nodes', $column), + "Expected column [$column] to be removed by the dropNestedSet() macro." + ); + } + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..8d376e3 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,72 @@ +set('database.default', $connection); + + $app['config']->set('database.connections.testbench', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => 'prfx_', + ]); + + $app['config']->set('database.connections.mysql', [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'nestedset'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'prefix' => 'prfx_', + ]); + + $app['config']->set('database.connections.pgsql', [ + 'driver' => 'pgsql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'nestedset'), + 'username' => env('DB_USERNAME', 'postgres'), + 'password' => env('DB_PASSWORD', ''), + 'prefix' => 'prfx_', + ]); + } + + /** + * Realign an auto-increment sequence after seeding rows with explicit ids. + * + * MySQL and SQLite advance the sequence automatically; PostgreSQL does not. + */ + protected function resetAutoIncrement(string $table): void + { + $connection = DB::connection(); + + if ($connection->getDriverName() !== 'pgsql') { + return; + } + + $prefixed = $connection->getTablePrefix().$table; + $max = (int) $connection->table($table)->max('id'); + + $connection->statement( + "SELECT setval(pg_get_serial_sequence(?, 'id'), ?)", + [$prefixed, max($max, 1)] + ); + } +} diff --git a/tests/data/categories.php b/tests/data/categories.php index 1f5b8ab..7ba0d14 100644 --- a/tests/data/categories.php +++ b/tests/data/categories.php @@ -1,15 +1,15 @@ 1, 'name' => 'store', '_lft' => 1, '_rgt' => 20, 'parent_id' => null), - array('id' => 2, 'name' => 'notebooks', '_lft' => 2, '_rgt' => 7, 'parent_id' => 1), - array('id' => 3, 'name' => 'apple', '_lft' => 3, '_rgt' => 4, 'parent_id' => 2), - array('id' => 4, 'name' => 'lenovo', '_lft' => 5, '_rgt' => 6, 'parent_id' => 2), - array('id' => 5, 'name' => 'mobile', '_lft' => 8, '_rgt' => 19, 'parent_id' => 1), - array('id' => 6, 'name' => 'nokia', '_lft' => 9, '_rgt' => 10, 'parent_id' => 5), - array('id' => 7, 'name' => 'samsung', '_lft' => 11, '_rgt' => 14, 'parent_id' => 5), - array('id' => 8, 'name' => 'galaxy', '_lft' => 12, '_rgt' => 13, 'parent_id' => 7), - array('id' => 9, 'name' => 'sony', '_lft' => 15, '_rgt' => 16, 'parent_id' => 5), - array('id' => 10, 'name' => 'lenovo', '_lft' => 17, '_rgt' => 18, 'parent_id' => 5), - array('id' => 11, 'name' => 'store_2', '_lft' => 21, '_rgt' => 22, 'parent_id' => null), -); \ No newline at end of file +return [ + ['id' => 1, 'name' => 'store', '_lft' => 1, '_rgt' => 20, 'parent_id' => null], + ['id' => 2, 'name' => 'notebooks', '_lft' => 2, '_rgt' => 7, 'parent_id' => 1], + ['id' => 3, 'name' => 'apple', '_lft' => 3, '_rgt' => 4, 'parent_id' => 2], + ['id' => 4, 'name' => 'lenovo', '_lft' => 5, '_rgt' => 6, 'parent_id' => 2], + ['id' => 5, 'name' => 'mobile', '_lft' => 8, '_rgt' => 19, 'parent_id' => 1], + ['id' => 6, 'name' => 'nokia', '_lft' => 9, '_rgt' => 10, 'parent_id' => 5], + ['id' => 7, 'name' => 'samsung', '_lft' => 11, '_rgt' => 14, 'parent_id' => 5], + ['id' => 8, 'name' => 'galaxy', '_lft' => 12, '_rgt' => 13, 'parent_id' => 7], + ['id' => 9, 'name' => 'sony', '_lft' => 15, '_rgt' => 16, 'parent_id' => 5], + ['id' => 10, 'name' => 'lenovo', '_lft' => 17, '_rgt' => 18, 'parent_id' => 5], + ['id' => 11, 'name' => 'store_2', '_lft' => 21, '_rgt' => 22, 'parent_id' => null], +]; diff --git a/tests/data/menu_items.php b/tests/data/menu_items.php index 5490f7d..2d0d990 100644 --- a/tests/data/menu_items.php +++ b/tests/data/menu_items.php @@ -1,8 +1,10 @@ - 1, 'menu_id' => 1, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1' ], - [ 'id' => 2, 'menu_id' => 1, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2' ], - [ 'id' => 5, 'menu_id' => 1, '_lft' => 4, '_rgt' => 5, 'parent_id' => 2, 'title' => 'menu item 3' ], - [ 'id' => 3, 'menu_id' => 2, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1' ], - [ 'id' => 4, 'menu_id' => 2, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2' ], - [ 'id' => 6, 'menu_id' => 2, '_lft' => 4, '_rgt' => 5, 'parent_id' => 4, 'title' => 'menu item 3' ], -]; \ No newline at end of file + 1, 'menu_id' => 1, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1'], + ['id' => 2, 'menu_id' => 1, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2'], + ['id' => 5, 'menu_id' => 1, '_lft' => 4, '_rgt' => 5, 'parent_id' => 2, 'title' => 'menu item 3'], + ['id' => 3, 'menu_id' => 2, '_lft' => 1, '_rgt' => 2, 'parent_id' => null, 'title' => 'menu item 1'], + ['id' => 4, 'menu_id' => 2, '_lft' => 3, '_rgt' => 6, 'parent_id' => null, 'title' => 'menu item 2'], + ['id' => 6, 'menu_id' => 2, '_lft' => 4, '_rgt' => 5, 'parent_id' => 4, 'title' => 'menu item 3'], +]; diff --git a/tests/models/Category.php b/tests/models/Category.php deleted file mode 100644 index 12d472c..0000000 --- a/tests/models/Category.php +++ /dev/null @@ -1,17 +0,0 @@ -