From d8cf22b53118648cc743d9e19d6804207bb0259b Mon Sep 17 00:00:00 2001 From: PaulZi Date: Fri, 11 Dec 2015 19:33:31 +0300 Subject: [PATCH] Add populateTree() method --- .travis.yml | 19 ++-- NestedSetsBehavior.php | 47 +++++++++ README.md | 60 ++++++++++- composer.lock | 110 ++++++++++----------- sample-migrations/m150722_150000_single_tree.php | 25 ----- sample-migrations/m150722_150100_multiple_tree.php | 26 ----- tests/NestedSetsBehaviorTestCase.php | 19 ++++ 7 files changed, 188 insertions(+), 118 deletions(-) delete mode 100644 sample-migrations/m150722_150000_single_tree.php delete mode 100644 sample-migrations/m150722_150100_multiple_tree.php diff --git a/.travis.yml b/.travis.yml index c8ca24c..6992493 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,26 +7,29 @@ php: - 7.0 - hhvm -matrix: - allow_failures: - - php: 7.0 - sudo: false install: - composer self-update - composer global require fxp/composer-asset-plugin:~1.0 - - composer install + - composer update --prefer-dist --no-interaction before_script: - mysql --version - psql --version - mysql -e 'create database test;' - psql -U postgres -c 'CREATE DATABASE test;'; + - | + if [ $TRAVIS_PHP_VERSION = '5.6' ]; then + PHPUNIT_FLAGS="--coverage-clover=coverage.clover" + fi script: - - vendor/bin/phpunit --coverage-clover=coverage.clover + - vendor/bin/phpunit $PHPUNIT_FLAGS after_script: - - wget https://scrutinizer-ci.com/ocular.phar - - php ocular.phar code-coverage:upload --format=php-clover coverage.clover + - | + if [ $TRAVIS_PHP_VERSION = '5.6' ]; then + wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover coverage.clover + fi diff --git a/NestedSetsBehavior.php b/NestedSetsBehavior.php index 33fcb0b..62d7a11 100644 --- a/NestedSetsBehavior.php +++ b/NestedSetsBehavior.php @@ -213,6 +213,53 @@ class NestedSetsBehavior extends Behavior } /** + * Populate children relations for self and all descendants + * @param int $depth = null + * @return static + */ + public function populateTree($depth = null) + { + /** @var ActiveRecord[]|static[] $nodes */ + if ($depth === null) { + $nodes = $this->owner->descendants; + } else { + $nodes = $this->getDescendants($depth)->all(); + } + + $relates = []; + $parents = [$this->owner->getAttribute($this->leftAttribute)]; + $prev = $this->owner->getAttribute($this->depthAttribute); + foreach($nodes as $node) + { + $depth = $node->getAttribute($this->depthAttribute); + if ($depth <= $prev) { + $parents = array_slice($parents, 0, $depth - $prev - 1); + } + + $key = end($parents); + if (!isset($relates[$key])) { + $relates[$key] = []; + } + $relates[$key][] = $node; + + $parents[] = $node->getAttribute($this->leftAttribute); + $prev = $depth; + } + + $nodes[$this->owner->getAttribute($this->leftAttribute)] = $this->owner; + foreach ($nodes as $node) { + $key = $node->getAttribute($this->leftAttribute); + if (isset($relates[$key])) { + $node->populateRelation('children', $relates[$key]); + } elseif ($depth === null) { + $node->populateRelation('children', []); + } + } + + return $this->owner; + } + + /** * @return bool */ public function isRoot() diff --git a/README.md b/README.md index 73f06b5..42436af 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,56 @@ or add to the `require` section of your `composer.json` file. -## Migrations +## Migrations example -Sample migrations are in the folder `sample-migrations`: +Single tree migration: -- `m150722_150000_single_tree.php` - for single tree tables; -- `m150722_150100_multiple_tree.php` - for multiple tree tables. +```php +class m150722_150000_single_tree extends Migration +{ + public function up() + { + $tableOptions = null; + if ($this->db->driverName === 'mysql') { + // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + } + $this->createTable('{{%single_tree}}', [ + 'id' => Schema::TYPE_PK, + 'lft' => Schema::TYPE_INTEGER . ' NOT NULL', + 'rgt' => Schema::TYPE_INTEGER . ' NOT NULL', + 'depth' => Schema::TYPE_INTEGER . ' NOT NULL', + ], $tableOptions); + $this->createIndex('lft', '{{%single_tree}}', ['lft', 'rgt']); + $this->createIndex('rgt', '{{%single_tree}}', ['rgt']); + } +} +``` + +Multiple tree migration: + +```php +class m150722_150100_multiple_tree extends Migration +{ + public function up() + { + $tableOptions = null; + if ($this->db->driverName === 'mysql') { + // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + } + $this->createTable('{{%multiple_tree}}', [ + 'id' => Schema::TYPE_PK, + 'tree' => Schema::TYPE_INTEGER . ' NULL', + 'lft' => Schema::TYPE_INTEGER . ' NOT NULL', + 'rgt' => Schema::TYPE_INTEGER . ' NOT NULL', + 'depth' => Schema::TYPE_INTEGER . ' NOT NULL', + ], $tableOptions); + $this->createIndex('lft', '{{%multiple_tree}}', ['tree', 'lft', 'rgt']); + $this->createIndex('rgt', '{{%multiple_tree}}', ['tree', 'rgt']); + } +} +``` ## Configuring @@ -136,6 +180,14 @@ $descendants = $node11->getDescendants(2, true)->all(); // get 2 levels of desce $descendants = $node11->getDescendants(3, false, true)->all(); // get 3 levels of descendants in back order ``` +To populate `children` relations for self and descendants of a node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$tree = $node11->populateTree(); // populate all levels +$tree = $node11->populateTree(2); // populate 2 levels of descendants +``` + To get the children of a node: ```php diff --git a/composer.lock b/composer.lock index b6c0af9..63f9bbd 100644 --- a/composer.lock +++ b/composer.lock @@ -133,16 +133,16 @@ }, { "name": "bower-asset/yii2-pjax", - "version": "v2.0.4", + "version": "v2.0.5", "source": { "type": "git", "url": "https://github.com/yiisoft/jquery-pjax.git", - "reference": "3f20897307cca046fca5323b318475ae9dac0ca0" + "reference": "6818718408086db6bdcf33649cecb86b6b4f9b67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/3f20897307cca046fca5323b318475ae9dac0ca0", - "reference": "3f20897307cca046fca5323b318475ae9dac0ca0", + "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/6818718408086db6bdcf33649cecb86b6b4f9b67", + "reference": "6818718408086db6bdcf33649cecb86b6b4f9b67", "shasum": "" }, "require": { @@ -629,16 +629,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "2.2.3", + "version": "2.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef1ca6835468857944d5c3b48fa503d5554cff2f" + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef1ca6835468857944d5c3b48fa503d5554cff2f", - "reference": "ef1ca6835468857944d5c3b48fa503d5554cff2f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", "shasum": "" }, "require": { @@ -687,7 +687,7 @@ "testing", "xunit" ], - "time": "2015-09-14 06:51:16" + "time": "2015-10-06 15:47:00" }, { "name": "phpunit/php-file-iterator", @@ -869,16 +869,16 @@ }, { "name": "phpunit/phpunit", - "version": "4.8.9", + "version": "4.8.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "73fad41adb5b7bc3a494bb930d90648df1d5e74b" + "reference": "7438c43bc2bbb2febe1723eb595b1c49283a26ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/73fad41adb5b7bc3a494bb930d90648df1d5e74b", - "reference": "73fad41adb5b7bc3a494bb930d90648df1d5e74b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7438c43bc2bbb2febe1723eb595b1c49283a26ad", + "reference": "7438c43bc2bbb2febe1723eb595b1c49283a26ad", "shasum": "" }, "require": { @@ -887,7 +887,7 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-spl": "*", - "php": ">=5.3.3", + "php": "~5.3.3|~5.4|~5.5|~5.6", "phpspec/prophecy": "^1.3.1", "phpunit/php-code-coverage": "~2.1", "phpunit/php-file-iterator": "~1.4", @@ -937,20 +937,20 @@ "testing", "xunit" ], - "time": "2015-09-20 12:56:44" + "time": "2015-12-10 07:48:52" }, { "name": "phpunit/phpunit-mock-objects", - "version": "2.3.7", + "version": "2.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "5e2645ad49d196e020b85598d7c97e482725786a" + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5e2645ad49d196e020b85598d7c97e482725786a", - "reference": "5e2645ad49d196e020b85598d7c97e482725786a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", "shasum": "" }, "require": { @@ -993,7 +993,7 @@ "mock", "xunit" ], - "time": "2015-08-19 09:14:08" + "time": "2015-10-02 06:51:40" }, { "name": "sebastian/comparator", @@ -1061,28 +1061,28 @@ }, { "name": "sebastian/diff", - "version": "1.3.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3" + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3", - "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "~4.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -1105,24 +1105,24 @@ } ], "description": "Diff implementation", - "homepage": "http://www.github.com/sebastianbergmann/diff", + "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ "diff" ], - "time": "2015-02-22 15:13:53" + "time": "2015-12-08 07:14:41" }, { "name": "sebastian/environment", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44" + "reference": "6e7133793a8e5a5714a551a8324337374be209df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6324c907ce7a52478eeeaede764f48733ef5ae44", - "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6e7133793a8e5a5714a551a8324337374be209df", + "reference": "6e7133793a8e5a5714a551a8324337374be209df", "shasum": "" }, "require": { @@ -1159,7 +1159,7 @@ "environment", "hhvm" ], - "time": "2015-08-03 06:14:51" + "time": "2015-12-02 08:37:27" }, { "name": "sebastian/exporter", @@ -1229,16 +1229,16 @@ }, { "name": "sebastian/global-state", - "version": "1.0.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01" + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01", - "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", "shasum": "" }, "require": { @@ -1276,20 +1276,20 @@ "keywords": [ "global state" ], - "time": "2014-10-06 09:23:50" + "time": "2015-10-12 03:26:01" }, { "name": "sebastian/recursion-context", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "994d4a811bafe801fb06dccbee797863ba2792ba" + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/994d4a811bafe801fb06dccbee797863ba2792ba", - "reference": "994d4a811bafe801fb06dccbee797863ba2792ba", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", "shasum": "" }, "require": { @@ -1329,7 +1329,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2015-06-21 08:04:50" + "time": "2015-11-11 19:50:13" }, { "name": "sebastian/version", @@ -1368,34 +1368,34 @@ }, { "name": "symfony/yaml", - "version": "v2.7.4", + "version": "v3.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/Yaml.git", - "reference": "2dc7b06c065df96cc686c66da2705e5e18aef661" + "url": "https://github.com/symfony/yaml.git", + "reference": "177a015cb0e19ff4a49e0e2e2c5fc1c1bee07002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/2dc7b06c065df96cc686c66da2705e5e18aef661", - "reference": "2dc7b06c065df96cc686c66da2705e5e18aef661", + "url": "https://api.github.com/repos/symfony/yaml/zipball/177a015cb0e19ff4a49e0e2e2c5fc1c1bee07002", + "reference": "177a015cb0e19ff4a49e0e2e2c5fc1c1bee07002", "shasum": "" }, "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" + "php": ">=5.5.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "3.0-dev" } }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" - } + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1413,7 +1413,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-08-24 07:13:45" + "time": "2015-11-30 12:36:17" } ], "aliases": [], diff --git a/sample-migrations/m150722_150000_single_tree.php b/sample-migrations/m150722_150000_single_tree.php deleted file mode 100644 index 55157b1..0000000 --- a/sample-migrations/m150722_150000_single_tree.php +++ /dev/null @@ -1,25 +0,0 @@ -db->driverName === 'mysql') { - // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci - $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; - } - - $this->createTable('{{%single_tree}}', [ - 'id' => Schema::TYPE_PK, - 'lft' => Schema::TYPE_INTEGER . ' NOT NULL', - 'rgt' => Schema::TYPE_INTEGER . ' NOT NULL', - 'depth' => Schema::TYPE_INTEGER . ' NOT NULL', - ], $tableOptions); - $this->createIndex('lft', '{{%single_tree}}', ['lft', 'rgt']); - $this->createIndex('rgt', '{{%single_tree}}', ['rgt']); - } -} diff --git a/sample-migrations/m150722_150100_multiple_tree.php b/sample-migrations/m150722_150100_multiple_tree.php deleted file mode 100644 index 0e82b61..0000000 --- a/sample-migrations/m150722_150100_multiple_tree.php +++ /dev/null @@ -1,26 +0,0 @@ -db->driverName === 'mysql') { - // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci - $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; - } - - $this->createTable('{{%multiple_tree}}', [ - 'id' => Schema::TYPE_PK, - 'tree' => Schema::TYPE_INTEGER . ' NULL', - 'lft' => Schema::TYPE_INTEGER . ' NOT NULL', - 'rgt' => Schema::TYPE_INTEGER . ' NOT NULL', - 'depth' => Schema::TYPE_INTEGER . ' NOT NULL', - ], $tableOptions); - $this->createIndex('lft', '{{%multiple_tree}}', ['tree', 'lft', 'rgt']); - $this->createIndex('rgt', '{{%multiple_tree}}', ['tree', 'rgt']); - } -} diff --git a/tests/NestedSetsBehaviorTestCase.php b/tests/NestedSetsBehaviorTestCase.php index d0d34d7..b265e04 100644 --- a/tests/NestedSetsBehaviorTestCase.php +++ b/tests/NestedSetsBehaviorTestCase.php @@ -115,6 +115,25 @@ class NestedSetsBehaviorTestCase extends BaseTestCase $this->assertEquals(null, MultipleTreeNode::findOne(19)->getNext()->one()); } + public function testPopulateTree() + { + $node = Node::findOne(2); + $node->populateTree(); + $this->assertEquals(true, $node->isRelationPopulated('children')); + $this->assertEquals(true, $node->children[0]->isRelationPopulated('children')); + $this->assertEquals(11, $node->children[0]->children[0]->id); + + $node = MultipleTreeNode::findOne(2); + $node->populateTree(1); + $this->assertEquals(true, $node->isRelationPopulated('children')); + $this->assertEquals(false, $node->children[0]->isRelationPopulated('children')); + $this->assertEquals(5, $node->children[0]->id); + + $node = Node::findOne(19); + $node->populateTree(); + $this->assertEquals(true, $node->isRelationPopulated('children')); + } + public function testIsRoot() { $this->assertTrue(Node::findOne(1)->isRoot());