diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..e9c4015 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,43 @@ +name: Unit Tests + +on: + push: + branches: + - master + pull_request: + branches: + - "*" + schedule: + - cron: '0 0 * * *' + +jobs: + php-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + + strategy: + fail-fast: false + matrix: + php: [8.3, 8.2] + + name: P${{ matrix.php }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: | + composer install -o --quiet + + - name: Execute Unit Tests + run: composer test diff --git a/.gitignore b/.gitignore index 8879189..6bef520 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.phar composer.lock .DS_Store +.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d7e4828..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: php - -php: - - 5.5 - - 5.6 - - 7.0 - -before_script: - - curl -s http://getcomposer.org/installer | php - - php composer.phar install --dev - -script: phpunit \ No newline at end of file diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index e1d837b..dcb7bb0 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -1,4 +1,14 @@ -### 4.2.8 +### 4.3.4 +* Support Laravel 5.8 + +### 4.3.3 +* Support Laravel 5.7 + +### 4.3.2 +* Support Laravel 5.6 +* Added `nestedSet` and `dropNestedSet` blueprint macros + +### 4.3.0 * Support Laravel 5.5 * Added `fixSubtree` and `rebuildSubtree` methods * Increased performance of tree rebuilding @@ -104,4 +114,4 @@ ### 1.1.0 * `Collection::toDictionary` is now obsolete. Use `Collection::groupBy`. -* Laravel 4.2 is required \ No newline at end of file +* Laravel 4.2 is required diff --git a/README.markdown b/README.markdown index 06ba4d7..f4d2d51 100644 --- a/README.markdown +++ b/README.markdown @@ -4,17 +4,18 @@ [](https://packagist.org/packages/kalnoy/nestedset) [](https://packagist.org/packages/kalnoy/nestedset) -This is a Laravel 4-5 package for working with trees in relational databases. - -* **Laravel 5.2, 5.3, 5.4, 5.5** is supported since v4 +This is a Laravel 4-11 package for working with trees in relational databases. + +* **Laravel 11.0** is supported since v7 +* **Laravel 10.0** is supported since v6.0.2 +* **Laravel 9.0** is supported since v6.0.1 +* **Laravel 8.0** is supported since v6.0.0 +* **Laravel 5.7, 5.8, 6.0, 7.0** is supported since v5 +* **Laravel 5.5, 5.6** is supported since v4.3 +* **Laravel 5.2, 5.3, 5.4** is supported since v4 * **Laravel 5.1** is supported in v3 * **Laravel 4** is supported in v2 -Although this project is completely free for use, I appreciate any support! - -- __[Donate via PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5TJUM7FYU5VR2)__ -- My Visa: 4276 0700 1073 4244 - __Contents:__ - [Theory](#what-are-nested-sets) @@ -384,6 +385,9 @@ position. Various constraints that can be applied to the query builder: - __whereIsRoot()__ to get only root nodes; +- __hasParent()__ to get non-root nodes; +- __whereIsLeaf()__ to get only leaves; +- __hasChildren()__ to get non-leave nodes; - __whereIsAfter($id)__ to get every node (not just siblings) that are after a node with specified id; - __whereIsBefore($id)__ to get every node that is before a node with specified id. @@ -571,17 +575,17 @@ protected function getScopeAttributes() } ``` -But now in order to execute some custom query, you need to provide attributes +But now, in order to execute some custom query, you need to provide attributes that are used for scoping: ```php MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OK MenuItem::descendantsOf($id)->get(); // WRONG: returns nodes from other scope -MenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); +MenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); // OK ``` When requesting nodes using model instance, scopes applied automatically based -on the attributes of that model. See examples: +on the attributes of that model: ```php $node = MenuItem::findOrFail($id); @@ -595,12 +599,13 @@ To get scoped query builder using instance: $node->newScopedQuery(); ``` -Note, that scoping is not required when retrieving model by primary key -(since the key is unique): +#### Scoping and eager loading + +Always use scoped query when eager loading: ```php -$node = MenuItem::findOrFail($id); // OK -$node = MenuItem::scoped([ 'menu_id' => 5 ])->findOrFail(); // OK, but redundant +MenuItem::scoped([ 'menu_id' => 5])->with('descendants')->findOrFail($id); // OK +MenuItem::with('descendants')->findOrFail($id); // WRONG ``` Requirements @@ -625,7 +630,21 @@ composer require kalnoy/nestedset #### The schema -You can use a method to add needed columns with default names: +For Laravel 5.5 and above users: + +```php +Schema::create('table', function (Blueprint $table) { + ... + $table->nestedSet(); +}); + +// To drop columns +Schema::table('table', function (Blueprint $table) { + $table->dropNestedSet(); +}); +``` + +For prior Laravel versions: ```php ... diff --git a/composer.json b/composer.json index efa2656..9f8d6c1 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "kalnoy/nestedset", - "description": "Nested Set Model for Laravel 4-5", + "description": "Nested Set Model for Laravel 5.7 and up", "keywords": ["laravel", "nested sets", "nsm", "database", "hierarchy"], "license": "MIT", @@ -12,10 +12,10 @@ ], "require": { - "php": ">=5.5.9", - "illuminate/support": "5.2 - 5.5", - "illuminate/database": "5.2 - 5.5", - "illuminate/events": "5.2 - 5.5" + "php": "^8.2", + "illuminate/support": "^11.0", + "illuminate/database": "^11.0", + "illuminate/events": "^11.0" }, "autoload": { @@ -24,15 +24,33 @@ } }, + "autoload-dev": { + "psr-4": { + "Kalnoy\\Nestedset\\Tests\\": "tests/" + } + }, + "require-dev": { - "phpunit/phpunit": "4.8.*" + "phpunit/phpunit": "10.*" }, "minimum-stability": "dev", + "prefer-stable": true, "extra": { "branch-alias": { - "dev-master": "v4.2.x-dev" + "dev-master": "v5.0.x-dev" + }, + + "laravel": { + "providers": [ + "Kalnoy\\Nestedset\\NestedSetServiceProvider" + ] } + }, + "scripts": { + "test": [ + "@php ./vendor/bin/phpunit" + ] } } diff --git a/phpunit.php b/phpunit.php index a9fa67a..7f72aff 100644 --- a/phpunit.php +++ b/phpunit.php @@ -8,4 +8,5 @@ $capsule->bootEloquent(); $capsule->setAsGlobal(); -include __DIR__.'/tests/models/Category.php'; \ No newline at end of file +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 80d7085..8021fb0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,24 +1,21 @@ -<?xml version="1.0" encoding="UTF-8"?> -<phpunit backupGlobals="false" - backupStaticAttributes="false" - bootstrap="phpunit.php" - colors="true" - convertErrorsToExceptions="true" - convertNoticesToExceptions="true" - convertWarningsToExceptions="true" - processIsolation="false" - stopOnFailure="true" - syntaxCheck="false" -> - <testsuites> - <testsuite name="Package Test Suite"> - <directory suffix=".php">./tests/</directory> - </testsuite> - </testsuites> - - <filter> - <whitelist> - <directory>./src</directory> - </whitelist> - </filter> -</phpunit> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?> +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + backupGlobals="false" + bootstrap="phpunit.php" + colors="true" + processIsolation="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" + cacheDirectory=".phpunit.cache" + backupStaticProperties="false" +> + <testsuites> + <testsuite name="Package Test Suite"> + <directory suffix=".php">./tests/</directory> + </testsuite> + </testsuites> + <source> + <include> + <directory>./src</directory> + </include> + </source> +</phpunit> diff --git a/src/AncestorsRelation.php b/src/AncestorsRelation.php index 089e382..b59fba2 100644 --- a/src/AncestorsRelation.php +++ b/src/AncestorsRelation.php @@ -15,7 +15,8 @@ public function addConstraints() { if ( ! static::$constraints) return; - $this->query->whereAncestorOf($this->parent)->defaultOrder(); + $this->query->whereAncestorOf($this->parent) + ->applyNestedSetScope(); } /** diff --git a/src/BaseRelation.php b/src/BaseRelation.php index b09e031..7f82f29 100644 --- a/src/BaseRelation.php +++ b/src/BaseRelation.php @@ -111,28 +111,15 @@ public function initRelation(array $models, $relation) return $models; } - /** - * @param EloquentBuilder $query - * @param EloquentBuilder $parent - * @param array $columns - * - * @return mixed - */ - public function getRelationQuery( - EloquentBuilder $query, EloquentBuilder $parent, - $columns = [ '*' ] - ) { - return $this->getRelationExistenceQuery($query, $parent, $columns); - } - /** * Get a relationship join table hash. * + * @param bool $incrementJoinCount * @return string */ - public function getRelationCountHash() + public function getRelationCountHash($incrementJoinCount = true) { - return 'nested_set_'.self::$selfJoinCount++; + return 'nested_set_'.($incrementJoinCount ? static::$selfJoinCount++ : static::$selfJoinCount); } /** @@ -154,10 +141,6 @@ public function getResults() */ public function addEagerConstraints(array $models) { - $model = reset($models); - - $this->query = $model->newScopedQuery(); - $this->query->whereNested(function (Builder $inner) use ($models) { // We will use this query in order to apply constraints to the // base query builder @@ -207,4 +190,16 @@ protected function matchForModel(Model $model, EloquentCollection $results) return $result; } -} \ No newline at end of file + + /** + * Get the plain foreign key. + * + * @return mixed + */ + public function getForeignKeyName() + { + // Return a stub value for relation + // resolvers which need this function. + return NestedSet::PARENT_ID; + } +} diff --git a/src/DescendantsRelation.php b/src/DescendantsRelation.php index 5240d3a..4c6457d 100644 --- a/src/DescendantsRelation.php +++ b/src/DescendantsRelation.php @@ -17,7 +17,8 @@ public function addConstraints() { if ( ! static::$constraints) return; - $this->query->whereDescendantOf($this->parent); + $this->query->whereDescendantOf($this->parent) + ->applyNestedSetScope(); } /** diff --git a/src/NestedSetServiceProvider.php b/src/NestedSetServiceProvider.php new file mode 100644 index 0000000..b4516f7 --- /dev/null +++ b/src/NestedSetServiceProvider.php @@ -0,0 +1,20 @@ +<?php + +namespace Kalnoy\Nestedset; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\ServiceProvider; + +class NestedSetServiceProvider extends ServiceProvider +{ + public function register() + { + Blueprint::macro('nestedSet', function () { + NestedSet::columns($this); + }); + + Blueprint::macro('dropNestedSet', function () { + NestedSet::dropColumns($this); + }); + } +} \ No newline at end of file diff --git a/src/NodeTrait.php b/src/NodeTrait.php index 167c001..c406ed8 100644 --- a/src/NodeTrait.php +++ b/src/NodeTrait.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Arr; use LogicException; trait NodeTrait @@ -248,7 +249,7 @@ public function children() */ public function descendants() { - return new DescendantsRelation($this->newScopedQuery(), $this); + return new DescendantsRelation($this->newQuery(), $this); } /** @@ -337,7 +338,7 @@ public function prevNodes() */ public function ancestors() { - return new AncestorsRelation($this->newScopedQuery(), $this); + return new AncestorsRelation($this->newQuery(), $this); } /** @@ -724,7 +725,7 @@ protected function getScopeAttributes() /** * @param array $attributes * - * @return self + * @return QueryBuilder */ public static function scoped(array $attributes) { @@ -752,7 +753,7 @@ public function newCollection(array $models = array()) */ public static function create(array $attributes = [], self $parent = null) { - $children = array_pull($attributes, 'children'); + $children = Arr::pull($attributes, 'children'); $instance = new static($attributes); @@ -1004,7 +1005,8 @@ public function getPrevSibling(array $columns = [ '*' ]) public function isDescendantOf(self $other) { return $this->getLft() > $other->getLft() && - $this->getLft() < $other->getRgt(); + $this->getLft() < $other->getRgt() && + $this->isSameScope($other); } /** @@ -1200,6 +1202,24 @@ protected function assertSameScope(self $node) } } + /** + * @param self $node + */ + protected function isSameScope(self $node): bool + { + if ( ! $scoped = $this->getScopeAttributes()) { + return true; + } + + foreach ($scoped as $attr) { + if ($this->getAttribute($attr) != $node->getAttribute($attr)) { + return false; + } + } + + return true; + } + /** * @param array|null $except * diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 56c702b..5be2e5b 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -86,9 +86,11 @@ public function whereIsRoot() */ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') { - $keyName = $this->model->getKeyName(); + $keyName = $this->model->getTable() . '.' . $this->model->getKeyName(); + $model = null; if (NestedSet::isNode($id)) { + $model = $id; $value = '?'; $this->query->addBinding($id->getRgt()); @@ -100,7 +102,7 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') ->toBase() ->select("_.".$this->model->getRgtName()) ->from($this->model->getTable().' as _') - ->where($keyName, '=', $id) + ->where($this->model->getKeyName(), '=', $id) ->limit(1); $this->query->mergeBindings($valueQuery); @@ -108,17 +110,22 @@ public function whereAncestorOf($id, $andSelf = false, $boolean = 'and') $value = '('.$valueQuery->toSql().')'; } - $this->query->whereNested(function ($inner) use ($value, $andSelf, $id) { + $this->query->whereNested(function ($inner) use ($model, $value, $andSelf, $id, $keyName) { list($lft, $rgt) = $this->wrappedColumns(); + $wrappedTable = $this->query->getGrammar()->wrapTable($this->model->getTable()); - $inner->whereRaw("{$value} between {$lft} and {$rgt}"); + $inner->whereRaw("{$value} between {$wrappedTable}.{$lft} and {$wrappedTable}.{$rgt}"); if ( ! $andSelf) { - $inner->where($this->model->getKeyName(), '<>', $id); + $inner->where($keyName, '<>', $id); + } + if ($model !== null) { + // we apply scope only when Node was passed as $id. + // In other cases, according to docs, query should be scoped() before calling this method + $model->applyNestedSetScope($inner); } }, $boolean); - return $this; } @@ -177,12 +184,13 @@ public function ancestorsAndSelf($id, array $columns = [ '*' ]) * @param array $values * @param string $boolean * @param bool $not + * @param Query $query * * @return $this */ - public function whereNodeBetween($values, $boolean = 'and', $not = false) + public function whereNodeBetween($values, $boolean = 'and', $not = false, $query = null) { - $this->query->whereBetween($this->model->getLftName(), $values, $boolean, $not); + ($query ?? $this->query)->whereBetween($this->model->getTable() . '.' . $this->model->getLftName(), $values, $boolean, $not); return $this; } @@ -216,19 +224,26 @@ public function orWhereNodeBetween($values) public function whereDescendantOf($id, $boolean = 'and', $not = false, $andSelf = false ) { - if (NestedSet::isNode($id)) { - $data = $id->getBounds(); - } else { - $data = $this->model->newNestedSetQuery() - ->getPlainNodeData($id, true); - } + $this->query->whereNested(function (Query $inner) use ($id, $andSelf, $not) { + if (NestedSet::isNode($id)) { + $id->applyNestedSetScope($inner); + $data = $id->getBounds(); + } else { + // we apply scope only when Node was passed as $id. + // In other cases, according to docs, query should be scoped() before calling this method + $data = $this->model->newNestedSetQuery() + ->getPlainNodeData($id, true); + } - // Don't include the node - if ( ! $andSelf) { - ++$data[0]; - } + // Don't include the node + if (!$andSelf) { + ++$data[0]; + } - return $this->whereNodeBetween($data, $boolean, $not); + return $this->whereNodeBetween($data, 'and', $not, $inner); + }, $boolean); + + return $this; } /** @@ -715,6 +730,7 @@ protected function getOdnessQuery() protected function getDuplicatesQuery() { $table = $this->wrappedTable(); + $keyName = $this->wrappedKey(); $firstAlias = 'c1'; $secondAlias = 'c2'; @@ -727,7 +743,7 @@ protected function getDuplicatesQuery() ->newNestedSetQuery($firstAlias) ->toBase() ->from($this->query->raw("{$table} as {$waFirst}, {$table} {$waSecond}")) - ->whereRaw("{$waFirst}.{$pk} < {$waSecond}.{$pk}") + ->whereRaw("{$waFirst}.{$keyName} < {$waSecond}.{$keyName}") ->whereNested(function (BaseQueryBuilder $inner) use ($waFirst, $waSecond) { list($lft, $rgt) = $this->wrappedColumns(); diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/NodeTest.php b/tests/NodeTest.php index 3f7bad0..16d6d9e 100644 --- a/tests/NodeTest.php +++ b/tests/NodeTest.php @@ -2,10 +2,11 @@ use Illuminate\Database\Capsule\Manager as Capsule; use Kalnoy\Nestedset\NestedSet; +use Kalnoy\Nestedset\Tests\Models\Category; -class NodeTest extends PHPUnit_Framework_TestCase +class NodeTest extends PHPUnit\Framework\TestCase { - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { $schema = Capsule::schema(); @@ -23,7 +24,7 @@ public static function setUpBeforeClass() Capsule::enableQueryLog(); } - public function setUp() + public function setUp(): void { $data = include __DIR__.'/data/categories.php'; @@ -36,7 +37,7 @@ public function setUp() date_default_timezone_set('America/Denver'); } - public function tearDown() + public function tearDown(): void { Capsule::table('categories')->truncate(); } @@ -104,12 +105,7 @@ public function assertNodeReceivesValidValues($node) ); } - /** - * @param $name - * - * @return \Category - */ - public function findCategory($name, $withTrashed = false) + public function findCategory(string $name, bool $withTrashed = false): Category { $q = new Category; @@ -221,32 +217,29 @@ public function testCategoryMovesUp() $this->assertNodeReceivesValidValues($node); } - /** - * @expectedException Exception - */ public function testFailsToInsertIntoChild() { + $this->expectException(Exception::class); + $node = $this->findCategory('notebooks'); $target = $node->children()->first(); $node->afterNode($target)->save(); } - /** - * @expectedException Exception - */ public function testFailsToAppendIntoItself() { + $this->expectException(Exception::class); + $node = $this->findCategory('notebooks'); $node->appendToNode($node)->save(); } - /** - * @expectedException Exception - */ public function testFailsToPrependIntoItself() { + $this->expectException(Exception::class); + $node = $this->findCategory('notebooks'); $node->prependTo($node)->save(); @@ -338,11 +331,10 @@ public function testParentIdAttributeAccessorAppendsNode() $this->assertTrue($node->isRoot()); } - /** - * @expectedException Exception - */ public function testFailsToSaveNodeUntilNotInserted() { + $this->expectException(Exception::class); + $node = new Category; $node->save(); } @@ -405,11 +397,10 @@ public function testSoftDeletedNodeisDeletedWhenParentIsDeleted() $this->assertNull($this->findCategory('sony')); } - /** - * @expectedException Exception - */ public function testFailsToSaveNodeUntilParentIsSaved() { + $this->expectException(Exception::class); + $node = new Category(array('title' => 'Node')); $parent = new Category(array('title' => 'Parent')); @@ -641,11 +632,10 @@ public function testDescendantsOfNonExistingNode() $this->assertTrue($node->getDescendants()->isEmpty()); } - /** - * @expectedException \Illuminate\Database\Eloquent\ModelNotFoundException - */ public function testWhereDescendantsOf() { + $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + Category::whereDescendantOf(124)->get(); } @@ -827,8 +817,6 @@ public function testRebuildSubtree() [ 'id' => '8' ], ]); - echo PHP_EOL.$fixed.PHP_EOL; - $this->assertTrue($fixed > 0); $this->assertTreeNotBroken(); @@ -854,11 +842,10 @@ public function testRebuildTreeWithDeletion() $this->assertTrue($nodes->count() > 1); } - /** - * @expectedException \Illuminate\Database\Eloquent\ModelNotFoundException - */ public function testRebuildFailsWithInvalidPK() { + $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + Category::rebuildTree([ [ 'id' => 24 ] ]); } @@ -872,7 +859,9 @@ public function testFlatTree() $this->assertEquals('galaxy', $tree[3]->name); } - public function testSeveralNodesModelWork() + // Commented, cause there is no assertion here and otherwise the test is marked as risky in PHPUnit 7. + // What's the purpose of this method? @todo: remove/update? + /*public function testSeveralNodesModelWork() { $category = new Category; @@ -885,7 +874,7 @@ public function testSeveralNodesModelWork() $duplicate->name = 'test'; $duplicate->saveAsRoot(); - } + }*/ public function testWhereIsLeaf() { diff --git a/tests/ScopedNodeTest.php b/tests/ScopedNodeTest.php index 0584ca7..aed1fe1 100644 --- a/tests/ScopedNodeTest.php +++ b/tests/ScopedNodeTest.php @@ -2,10 +2,11 @@ use Illuminate\Database\Capsule\Manager as Capsule; use Kalnoy\Nestedset\NestedSet; +use Kalnoy\Nestedset\Tests\Models\MenuItem; -class ScopedNodeTest extends PHPUnit_Framework_TestCase +class ScopedNodeTest extends PHPUnit\Framework\TestCase { - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { $schema = Capsule::schema(); @@ -23,7 +24,7 @@ public static function setUpBeforeClass() Capsule::enableQueryLog(); } - public function setUp() + public function setUp(): void { $data = include __DIR__.'/data/menu_items.php'; @@ -36,7 +37,7 @@ public function setUp() date_default_timezone_set('America/Denver'); } - public function tearDown() + public function tearDown(): void { Capsule::table('menu_items')->truncate(); } @@ -98,6 +99,13 @@ public function testDescendants() $this->assertEquals(1, $result->count()); $this->assertEquals(5, $result->first()->getKey()); + + $node = MenuItem::scoped([ 'menu_id' => 1 ])->with('descendants')->find(2); + + $result = $node->descendants; + + $this->assertEquals(1, $result->count()); + $this->assertEquals(5, $result->first()->getKey()); } public function testAncestors() @@ -109,7 +117,7 @@ public function testAncestors() $this->assertEquals(1, $result->count()); $this->assertEquals(2, $result->first()->getKey()); - $node = MenuItem::with('ancestors')->find(5); + $node = MenuItem::scoped([ 'menu_id' => 1 ])->with('ancestors')->find(5); $result = $node->ancestors; @@ -152,11 +160,10 @@ public function testInsertion() $this->assertOtherScopeNotAffected(); } - /** - * @expectedException \Illuminate\Database\Eloquent\ModelNotFoundException - */ public function testInsertionToParentFromOtherScope() { + $this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + $node = MenuItem::create([ 'menu_id' => 2, 'parent_id' => 5 ]); } @@ -186,31 +193,47 @@ protected function assertOtherScopeNotAffected() $this->assertEquals(1, $node->getLft()); } - public function testRebuildsTree() + // Commented, cause there is no assertion here and otherwise the test is marked as risky in PHPUnit 7. + // What's the purpose of this method? @todo: remove/update? + /*public function testRebuildsTree() { $data = []; MenuItem::scoped([ 'menu_id' => 2 ])->rebuildTree($data); - } + }*/ - /** - * @expectedException LogicException - */ public function testAppendingToAnotherScopeFails() { + $this->expectException(LogicException::class); + $a = MenuItem::find(1); $b = MenuItem::find(3); $a->appendToNode($b)->save(); } - /** - * @expectedException LogicException - */ public function testInsertingBeforeAnotherScopeFails() { + $this->expectException(LogicException::class); + $a = MenuItem::find(1); $b = MenuItem::find(3); $a->insertAfterNode($b); } + + public function testEagerLoadingAncestorsWithScope() + { + $filteredNodes = MenuItem::where('title', 'menu item 3')->with(['ancestors'])->get(); + + $this->assertEquals(2, $filteredNodes->find(5)->ancestors[0]->id); + $this->assertEquals(4, $filteredNodes->find(6)->ancestors[0]->id); + } + + public function testEagerLoadingDescendantsWithScope() + { + $filteredNodes = MenuItem::where('title', 'menu item 2')->with(['descendants'])->get(); + + $this->assertEquals(5, $filteredNodes->find(2)->descendants[0]->id); + $this->assertEquals(6, $filteredNodes->find(4)->descendants[0]->id); + } } \ No newline at end of file diff --git a/tests/data/categories.php b/tests/data/categories.php index 1f5b8ab..cd16d40 100644 --- a/tests/data/categories.php +++ b/tests/data/categories.php @@ -1,15 +1,15 @@ <?php -return array( - array('id' => 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], +]; \ No newline at end of file diff --git a/tests/data/menu_items.php b/tests/data/menu_items.php index 5490f7d..fba6acb 100644 --- a/tests/data/menu_items.php +++ b/tests/data/menu_items.php @@ -1,8 +1,10 @@ -<?php return [ - [ 'id' => 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' ], +<?php + +return [ + ['id' => 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 diff --git a/tests/models/Category.php b/tests/models/Category.php index bcce8e9..0c35daa 100644 --- a/tests/models/Category.php +++ b/tests/models/Category.php @@ -1,12 +1,17 @@ <?php +namespace Kalnoy\Nestedset\Tests\Models; + use \Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; +use Kalnoy\Nestedset\NodeTrait; class Category extends Model { - use \Illuminate\Database\Eloquent\SoftDeletes, \Kalnoy\Nestedset\NodeTrait; + use SoftDeletes; + use NodeTrait; - protected $fillable = array('name', 'parent_id'); + protected $fillable = ['name', 'parent_id']; public $timestamps = false; diff --git a/tests/models/DuplicateCategory.php b/tests/models/DuplicateCategory.php index a6f619a..f7f0e31 100644 --- a/tests/models/DuplicateCategory.php +++ b/tests/models/DuplicateCategory.php @@ -1,12 +1,16 @@ <?php +namespace Kalnoy\Nestedset\Tests\Models; + +use Kalnoy\Nestedset\NodeTrait; + class DuplicateCategory extends \Illuminate\Database\Eloquent\Model { - use \Kalnoy\Nestedset\NodeTrait; + use NodeTrait; protected $table = 'categories'; - protected $fillable = [ 'name' ]; + protected $fillable = ['name']; public $timestamps = false; } \ No newline at end of file diff --git a/tests/models/MenuItem.php b/tests/models/MenuItem.php index 2e10a55..27c87ac 100644 --- a/tests/models/MenuItem.php +++ b/tests/models/MenuItem.php @@ -1,13 +1,16 @@ <?php +namespace Kalnoy\Nestedset\Tests\Models; + +use Kalnoy\Nestedset\NodeTrait; class MenuItem extends \Illuminate\Database\Eloquent\Model { - use \Kalnoy\Nestedset\NodeTrait; + use NodeTrait; public $timestamps = false; - protected $fillable = ['menu_id','parent_id']; + protected $fillable = ['menu_id', 'parent_id']; public static function resetActionsPerformed() {