├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml.dist ├── src ├── NestedSetsBehavior.php └── NestedSetsQueryBehavior.php └── tests ├── DatabaseTestCase.php ├── MysqlNestedSetsBehaviorTest.php ├── MysqlNestedSetsQueryBehaviorTest.php ├── NestedSetsBehaviorTest.php ├── NestedSetsQueryBehaviorTest.php ├── bootstrap.php ├── data ├── clean.xml ├── test-append-to-exists-another-tree.xml ├── test-append-to-exists-down.xml ├── test-append-to-exists-up.xml ├── test-append-to-new.xml ├── test-children-multiple-tree-with-depth.php ├── test-children-multiple-tree.php ├── test-children-with-depth.php ├── test-children.php ├── test-delete-with-children.xml ├── test-delete.xml ├── test-insert-after-exists-another-tree.xml ├── test-insert-after-exists-down.xml ├── test-insert-after-exists-up.xml ├── test-insert-after-new.xml ├── test-insert-before-exists-another-tree.xml ├── test-insert-before-exists-down.xml ├── test-insert-before-exists-up.xml ├── test-insert-before-new.xml ├── test-leaves-multiple-tree-query.php ├── test-leaves-multiple-tree.php ├── test-leaves-query.php ├── test-leaves.php ├── test-make-root-exists.xml ├── test-make-root-new.xml ├── test-next-multiple-tree.php ├── test-next.php ├── test-parents-multiple-tree-with-depth.php ├── test-parents-multiple-tree.php ├── test-parents-with-depth.php ├── test-parents.php ├── test-prepend-to-exists-another-tree.xml ├── test-prepend-to-exists-down.xml ├── test-prepend-to-exists-up.xml ├── test-prepend-to-new.xml ├── test-prev-multiple-tree.php ├── test-prev.php ├── test-roots-multiple-tree-query.php ├── test-roots-query.php └── test.xml ├── migrations ├── mysql.sql └── sqlite.sql └── models ├── MultipleTree.php ├── MultipleTreeQuery.php ├── Tree.php └── TreeQuery.php /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | 21 | # Mac DS_Store Files 22 | .DS_Store 23 | 24 | # phpunit itself is not needed 25 | phpunit.phar 26 | # local phpunit config 27 | /phpunit.xml 28 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | tools: 2 | external_code_coverage: 3 | timeout: 1800 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - hhvm 8 | 9 | install: 10 | - composer self-update 11 | - composer global require fxp/composer-asset-plugin:~1.0 12 | - composer install 13 | 14 | before_script: 15 | - mysql -e 'create database yii2_nested_sets_test;' 16 | 17 | script: 18 | - vendor/bin/phpunit --coverage-clover=coverage.clover 19 | 20 | after_script: 21 | - wget https://scrutinizer-ci.com/ocular.phar 22 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The nested sets behavior for the Yii framework is free software. 2 | It is released under the terms of the following BSD License. 3 | 4 | Copyright © 2015, Alexander Kochetov (https://github.com/creocoder) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | * Neither the name of Yii Software LLC nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nested Sets Behavior for Yii 2 2 | 3 | [![Build Status](https://img.shields.io/travis/creocoder/yii2-nested-sets/master.svg?style=flat-square)](https://travis-ci.org/creocoder/yii2-nested-sets) 4 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/creocoder/yii2-nested-sets/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/creocoder/yii2-nested-sets/?branch=master) 5 | [![Packagist Version](https://img.shields.io/packagist/v/creocoder/yii2-nested-sets.svg?style=flat-square)](https://packagist.org/packages/creocoder/yii2-nested-sets) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/creocoder/yii2-nested-sets.svg?style=flat-square)](https://packagist.org/packages/creocoder/yii2-nested-sets) 7 | 8 | A modern nested sets behavior for the Yii framework utilizing the Modified Preorder Tree Traversal algorithm. 9 | 10 | ## Installation 11 | 12 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 13 | 14 | Either run 15 | 16 | ```bash 17 | $ composer require creocoder/yii2-nested-sets 18 | ``` 19 | 20 | or add 21 | 22 | ``` 23 | "creocoder/yii2-nested-sets": "0.9.*" 24 | ``` 25 | 26 | to the `require` section of your `composer.json` file. 27 | 28 | ## Migrations 29 | 30 | Run the following command 31 | 32 | ```bash 33 | $ yii migrate/create create_menu_table 34 | ``` 35 | 36 | Open the `/path/to/migrations/m_xxxxxx_xxxxxx_create_menu_table.php` file, 37 | inside the `up()` method add the following 38 | 39 | ```php 40 | $this->createTable('{{%menu}}', [ 41 | 'id' => $this->primaryKey(), 42 | //'tree' => $this->integer()->notNull(), 43 | 'lft' => $this->integer()->notNull(), 44 | 'rgt' => $this->integer()->notNull(), 45 | 'depth' => $this->integer()->notNull(), 46 | 'name' => $this->string()->notNull(), 47 | ]); 48 | ``` 49 | 50 | To use multiple tree mode uncomment `tree` field. 51 | 52 | ## Configuring 53 | 54 | Configure model as follows 55 | 56 | ```php 57 | use creocoder\nestedsets\NestedSetsBehavior; 58 | 59 | class Menu extends \yii\db\ActiveRecord 60 | { 61 | public function behaviors() { 62 | return [ 63 | 'tree' => [ 64 | 'class' => NestedSetsBehavior::className(), 65 | // 'treeAttribute' => 'tree', 66 | // 'leftAttribute' => 'lft', 67 | // 'rightAttribute' => 'rgt', 68 | // 'depthAttribute' => 'depth', 69 | ], 70 | ]; 71 | } 72 | 73 | public function transactions() 74 | { 75 | return [ 76 | self::SCENARIO_DEFAULT => self::OP_ALL, 77 | ]; 78 | } 79 | 80 | public static function find() 81 | { 82 | return new MenuQuery(get_called_class()); 83 | } 84 | } 85 | ``` 86 | 87 | To use multiple tree mode uncomment `treeAttribute` array key inside `behaviors()` method. 88 | 89 | Configure query class as follows 90 | 91 | ```php 92 | use creocoder\nestedsets\NestedSetsQueryBehavior; 93 | 94 | class MenuQuery extends \yii\db\ActiveQuery 95 | { 96 | public function behaviors() { 97 | return [ 98 | NestedSetsQueryBehavior::className(), 99 | ]; 100 | } 101 | } 102 | ``` 103 | 104 | ## Usage 105 | 106 | ### Making a root node 107 | 108 | To make a root node 109 | 110 | ```php 111 | $countries = new Menu(['name' => 'Countries']); 112 | $countries->makeRoot(); 113 | ``` 114 | 115 | The tree will look like this 116 | 117 | ``` 118 | - Countries 119 | ``` 120 | 121 | ### Prepending a node as the first child of another node 122 | 123 | To prepend a node as the first child of another node 124 | 125 | ```php 126 | $russia = new Menu(['name' => 'Russia']); 127 | $russia->prependTo($countries); 128 | ``` 129 | 130 | The tree will look like this 131 | 132 | ``` 133 | - Countries 134 | - Russia 135 | ``` 136 | 137 | ### Appending a node as the last child of another node 138 | 139 | To append a node as the last child of another node 140 | 141 | ```php 142 | $australia = new Menu(['name' => 'Australia']); 143 | $australia->appendTo($countries); 144 | ``` 145 | 146 | The tree will look like this 147 | 148 | ``` 149 | - Countries 150 | - Russia 151 | - Australia 152 | ``` 153 | 154 | ### Inserting a node before another node 155 | 156 | To insert a node before another node 157 | 158 | ```php 159 | $newZeeland = new Menu(['name' => 'New Zeeland']); 160 | $newZeeland->insertBefore($australia); 161 | ``` 162 | 163 | The tree will look like this 164 | 165 | ``` 166 | - Countries 167 | - Russia 168 | - New Zeeland 169 | - Australia 170 | ``` 171 | 172 | ### Inserting a node after another node 173 | 174 | To insert a node after another node 175 | 176 | ```php 177 | $unitedStates = new Menu(['name' => 'United States']); 178 | $unitedStates->insertAfter($australia); 179 | ``` 180 | 181 | The tree will look like this 182 | ``` 183 | - Countries 184 | - Russia 185 | - New Zeeland 186 | - Australia 187 | - United States 188 | ``` 189 | 190 | ### Getting the root nodes 191 | 192 | To get all the root nodes 193 | 194 | ```php 195 | $roots = Menu::find()->roots()->all(); 196 | ``` 197 | 198 | ### Getting the leaves nodes 199 | 200 | To get all the leaves nodes 201 | 202 | ```php 203 | $leaves = Menu::find()->leaves()->all(); 204 | ``` 205 | 206 | To get all the leaves of a node 207 | 208 | ```php 209 | $countries = Menu::findOne(['name' => 'Countries']); 210 | $leaves = $countries->leaves()->all(); 211 | ``` 212 | 213 | ### Getting children of a node 214 | 215 | To get all the children of a node 216 | 217 | ```php 218 | $countries = Menu::findOne(['name' => 'Countries']); 219 | $children = $countries->children()->all(); 220 | ``` 221 | 222 | To get the first level children of a node 223 | 224 | ```php 225 | $countries = Menu::findOne(['name' => 'Countries']); 226 | $children = $countries->children(1)->all(); 227 | ``` 228 | 229 | ### Getting parents of a node 230 | 231 | To get all the parents of a node 232 | 233 | ```php 234 | $countries = Menu::findOne(['name' => 'Countries']); 235 | $parents = $countries->parents()->all(); 236 | ``` 237 | 238 | To get the first parent of a node 239 | 240 | ```php 241 | $countries = Menu::findOne(['name' => 'Countries']); 242 | $parent = $countries->parents(1)->one(); 243 | ``` 244 | 245 | ## Donating 246 | 247 | Support this project and [others by creocoder](https://gratipay.com/creocoder/) via [gratipay](https://gratipay.com/creocoder/). 248 | 249 | [![Support via Gratipay](https://cdn.rawgit.com/gratipay/gratipay-badge/2.3.0/dist/gratipay.svg)](https://gratipay.com/creocoder/) 250 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "creocoder/yii2-nested-sets", 3 | "description": "The nested sets behavior for the Yii framework", 4 | "keywords": [ 5 | "yii2", 6 | "nested sets" 7 | ], 8 | "type": "yii2-extension", 9 | "license": "BSD-3-Clause", 10 | "authors": [ 11 | { 12 | "name": "Alexander Kochetov", 13 | "email": "creocoder@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "yiisoft/yii2": "~2.0.0" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "~4.0", 21 | "phpunit/dbunit": "~1.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "creocoder\\nestedsets\\": "src" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | src 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/NestedSetsBehavior.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class NestedSetsBehavior extends Behavior 24 | { 25 | const OPERATION_MAKE_ROOT = 'makeRoot'; 26 | const OPERATION_PREPEND_TO = 'prependTo'; 27 | const OPERATION_APPEND_TO = 'appendTo'; 28 | const OPERATION_INSERT_BEFORE = 'insertBefore'; 29 | const OPERATION_INSERT_AFTER = 'insertAfter'; 30 | const OPERATION_DELETE_WITH_CHILDREN = 'deleteWithChildren'; 31 | 32 | /** 33 | * @var string|false 34 | */ 35 | public $treeAttribute = false; 36 | /** 37 | * @var string 38 | */ 39 | public $leftAttribute = 'lft'; 40 | /** 41 | * @var string 42 | */ 43 | public $rightAttribute = 'rgt'; 44 | /** 45 | * @var string 46 | */ 47 | public $depthAttribute = 'depth'; 48 | /** 49 | * @var string|null 50 | */ 51 | protected $operation; 52 | /** 53 | * @var ActiveRecord|null 54 | */ 55 | protected $node; 56 | 57 | /** 58 | * @inheritdoc 59 | */ 60 | public function events() 61 | { 62 | return [ 63 | ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert', 64 | ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert', 65 | ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate', 66 | ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate', 67 | ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete', 68 | ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete', 69 | ]; 70 | } 71 | 72 | /** 73 | * Creates the root node if the active record is new or moves it 74 | * as the root node. 75 | * @param boolean $runValidation 76 | * @param array $attributes 77 | * @return boolean 78 | */ 79 | public function makeRoot($runValidation = true, $attributes = null) 80 | { 81 | $this->operation = self::OPERATION_MAKE_ROOT; 82 | 83 | return $this->owner->save($runValidation, $attributes); 84 | } 85 | 86 | /** 87 | * Creates a node as the first child of the target node if the active 88 | * record is new or moves it as the first child of the target node. 89 | * @param ActiveRecord $node 90 | * @param boolean $runValidation 91 | * @param array $attributes 92 | * @return boolean 93 | */ 94 | public function prependTo($node, $runValidation = true, $attributes = null) 95 | { 96 | $this->operation = self::OPERATION_PREPEND_TO; 97 | $this->node = $node; 98 | 99 | return $this->owner->save($runValidation, $attributes); 100 | } 101 | 102 | /** 103 | * Creates a node as the last child of the target node if the active 104 | * record is new or moves it as the last child of the target node. 105 | * @param ActiveRecord $node 106 | * @param boolean $runValidation 107 | * @param array $attributes 108 | * @return boolean 109 | */ 110 | public function appendTo($node, $runValidation = true, $attributes = null) 111 | { 112 | $this->operation = self::OPERATION_APPEND_TO; 113 | $this->node = $node; 114 | 115 | return $this->owner->save($runValidation, $attributes); 116 | } 117 | 118 | /** 119 | * Creates a node as the previous sibling of the target node if the active 120 | * record is new or moves it as the previous sibling of the target node. 121 | * @param ActiveRecord $node 122 | * @param boolean $runValidation 123 | * @param array $attributes 124 | * @return boolean 125 | */ 126 | public function insertBefore($node, $runValidation = true, $attributes = null) 127 | { 128 | $this->operation = self::OPERATION_INSERT_BEFORE; 129 | $this->node = $node; 130 | 131 | return $this->owner->save($runValidation, $attributes); 132 | } 133 | 134 | /** 135 | * Creates a node as the next sibling of the target node if the active 136 | * record is new or moves it as the next sibling of the target node. 137 | * @param ActiveRecord $node 138 | * @param boolean $runValidation 139 | * @param array $attributes 140 | * @return boolean 141 | */ 142 | public function insertAfter($node, $runValidation = true, $attributes = null) 143 | { 144 | $this->operation = self::OPERATION_INSERT_AFTER; 145 | $this->node = $node; 146 | 147 | return $this->owner->save($runValidation, $attributes); 148 | } 149 | 150 | /** 151 | * Deletes a node and its children. 152 | * @return integer|false the number of rows deleted or false if 153 | * the deletion is unsuccessful for some reason. 154 | * @throws \Exception 155 | */ 156 | public function deleteWithChildren() 157 | { 158 | $this->operation = self::OPERATION_DELETE_WITH_CHILDREN; 159 | 160 | if (!$this->owner->isTransactional(ActiveRecord::OP_DELETE)) { 161 | return $this->deleteWithChildrenInternal(); 162 | } 163 | 164 | $transaction = $this->owner->getDb()->beginTransaction(); 165 | 166 | try { 167 | $result = $this->deleteWithChildrenInternal(); 168 | 169 | if ($result === false) { 170 | $transaction->rollBack(); 171 | } else { 172 | $transaction->commit(); 173 | } 174 | 175 | return $result; 176 | } catch (\Exception $e) { 177 | $transaction->rollBack(); 178 | throw $e; 179 | } 180 | } 181 | 182 | /** 183 | * @return integer|false the number of rows deleted or false if 184 | * the deletion is unsuccessful for some reason. 185 | */ 186 | protected function deleteWithChildrenInternal() 187 | { 188 | if (!$this->owner->beforeDelete()) { 189 | return false; 190 | } 191 | 192 | $condition = [ 193 | 'and', 194 | ['>=', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)], 195 | ['<=', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)] 196 | ]; 197 | 198 | $this->applyTreeAttributeCondition($condition); 199 | $result = $this->owner->deleteAll($condition); 200 | $this->owner->setOldAttributes(null); 201 | $this->owner->afterDelete(); 202 | 203 | return $result; 204 | } 205 | 206 | /** 207 | * Gets the parents of the node. 208 | * @param integer|null $depth the depth 209 | * @return \yii\db\ActiveQuery 210 | */ 211 | public function parents($depth = null) 212 | { 213 | $condition = [ 214 | 'and', 215 | ['<', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)], 216 | ['>', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)], 217 | ]; 218 | 219 | if ($depth !== null) { 220 | $condition[] = ['>=', $this->depthAttribute, $this->owner->getAttribute($this->depthAttribute) - $depth]; 221 | } 222 | 223 | $this->applyTreeAttributeCondition($condition); 224 | 225 | return $this->owner->find()->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]); 226 | } 227 | 228 | /** 229 | * Gets the children of the node. 230 | * @param integer|null $depth the depth 231 | * @return \yii\db\ActiveQuery 232 | */ 233 | public function children($depth = null) 234 | { 235 | $condition = [ 236 | 'and', 237 | ['>', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)], 238 | ['<', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)], 239 | ]; 240 | 241 | if ($depth !== null) { 242 | $condition[] = ['<=', $this->depthAttribute, $this->owner->getAttribute($this->depthAttribute) + $depth]; 243 | } 244 | 245 | $this->applyTreeAttributeCondition($condition); 246 | 247 | return $this->owner->find()->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]); 248 | } 249 | 250 | /** 251 | * Gets the leaves of the node. 252 | * @return \yii\db\ActiveQuery 253 | */ 254 | public function leaves() 255 | { 256 | $condition = [ 257 | 'and', 258 | ['>', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)], 259 | ['<', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)], 260 | [$this->rightAttribute => new Expression($this->owner->getDb()->quoteColumnName($this->leftAttribute) . '+ 1')], 261 | ]; 262 | 263 | $this->applyTreeAttributeCondition($condition); 264 | 265 | return $this->owner->find()->andWhere($condition)->addOrderBy([$this->leftAttribute => SORT_ASC]); 266 | } 267 | 268 | /** 269 | * Gets the previous sibling of the node. 270 | * @return \yii\db\ActiveQuery 271 | */ 272 | public function prev() 273 | { 274 | $condition = [$this->rightAttribute => $this->owner->getAttribute($this->leftAttribute) - 1]; 275 | $this->applyTreeAttributeCondition($condition); 276 | 277 | return $this->owner->find()->andWhere($condition); 278 | } 279 | 280 | /** 281 | * Gets the next sibling of the node. 282 | * @return \yii\db\ActiveQuery 283 | */ 284 | public function next() 285 | { 286 | $condition = [$this->leftAttribute => $this->owner->getAttribute($this->rightAttribute) + 1]; 287 | $this->applyTreeAttributeCondition($condition); 288 | 289 | return $this->owner->find()->andWhere($condition); 290 | } 291 | 292 | /** 293 | * Determines whether the node is root. 294 | * @return boolean whether the node is root 295 | */ 296 | public function isRoot() 297 | { 298 | return $this->owner->getAttribute($this->leftAttribute) == 1; 299 | } 300 | 301 | /** 302 | * Determines whether the node is child of the parent node. 303 | * @param ActiveRecord $node the parent node 304 | * @return boolean whether the node is child of the parent node 305 | */ 306 | public function isChildOf($node) 307 | { 308 | $result = $this->owner->getAttribute($this->leftAttribute) > $node->getAttribute($this->leftAttribute) 309 | && $this->owner->getAttribute($this->rightAttribute) < $node->getAttribute($this->rightAttribute); 310 | 311 | if ($result && $this->treeAttribute !== false) { 312 | $result = $this->owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute); 313 | } 314 | 315 | return $result; 316 | } 317 | 318 | /** 319 | * Determines whether the node is leaf. 320 | * @return boolean whether the node is leaf 321 | */ 322 | public function isLeaf() 323 | { 324 | return $this->owner->getAttribute($this->rightAttribute) - $this->owner->getAttribute($this->leftAttribute) === 1; 325 | } 326 | 327 | /** 328 | * @throws NotSupportedException 329 | */ 330 | public function beforeInsert() 331 | { 332 | if ($this->node !== null && !$this->node->getIsNewRecord()) { 333 | $this->node->refresh(); 334 | } 335 | 336 | switch ($this->operation) { 337 | case self::OPERATION_MAKE_ROOT: 338 | $this->beforeInsertRootNode(); 339 | break; 340 | case self::OPERATION_PREPEND_TO: 341 | $this->beforeInsertNode($this->node->getAttribute($this->leftAttribute) + 1, 1); 342 | break; 343 | case self::OPERATION_APPEND_TO: 344 | $this->beforeInsertNode($this->node->getAttribute($this->rightAttribute), 1); 345 | break; 346 | case self::OPERATION_INSERT_BEFORE: 347 | $this->beforeInsertNode($this->node->getAttribute($this->leftAttribute), 0); 348 | break; 349 | case self::OPERATION_INSERT_AFTER: 350 | $this->beforeInsertNode($this->node->getAttribute($this->rightAttribute) + 1, 0); 351 | break; 352 | default: 353 | throw new NotSupportedException('Method "'. get_class($this->owner) . '::insert" is not supported for inserting new nodes.'); 354 | } 355 | } 356 | 357 | /** 358 | * @throws Exception 359 | */ 360 | protected function beforeInsertRootNode() 361 | { 362 | if ($this->treeAttribute === false && $this->owner->find()->roots()->exists()) { 363 | throw new Exception('Can not create more than one root when "treeAttribute" is false.'); 364 | } 365 | 366 | $this->owner->setAttribute($this->leftAttribute, 1); 367 | $this->owner->setAttribute($this->rightAttribute, 2); 368 | $this->owner->setAttribute($this->depthAttribute, 0); 369 | } 370 | 371 | /** 372 | * @param integer $value 373 | * @param integer $depth 374 | * @throws Exception 375 | */ 376 | protected function beforeInsertNode($value, $depth) 377 | { 378 | if ($this->node->getIsNewRecord()) { 379 | throw new Exception('Can not create a node when the target node is new record.'); 380 | } 381 | 382 | if ($depth === 0 && $this->node->isRoot()) { 383 | throw new Exception('Can not create a node when the target node is root.'); 384 | } 385 | 386 | $this->owner->setAttribute($this->leftAttribute, $value); 387 | $this->owner->setAttribute($this->rightAttribute, $value + 1); 388 | $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute) + $depth); 389 | 390 | if ($this->treeAttribute !== false) { 391 | $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute)); 392 | } 393 | 394 | $this->shiftLeftRightAttribute($value, 2); 395 | } 396 | 397 | /** 398 | * @throws Exception 399 | */ 400 | public function afterInsert() 401 | { 402 | if ($this->operation === self::OPERATION_MAKE_ROOT && $this->treeAttribute !== false) { 403 | $this->owner->setAttribute($this->treeAttribute, $this->owner->getPrimaryKey()); 404 | $primaryKey = $this->owner->primaryKey(); 405 | 406 | if (!isset($primaryKey[0])) { 407 | throw new Exception('"' . get_class($this->owner) . '" must have a primary key.'); 408 | } 409 | 410 | $this->owner->updateAll( 411 | [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)], 412 | [$primaryKey[0] => $this->owner->getAttribute($this->treeAttribute)] 413 | ); 414 | } 415 | 416 | $this->operation = null; 417 | $this->node = null; 418 | } 419 | 420 | /** 421 | * @throws Exception 422 | */ 423 | public function beforeUpdate() 424 | { 425 | if ($this->node !== null && !$this->node->getIsNewRecord()) { 426 | $this->node->refresh(); 427 | } 428 | 429 | switch ($this->operation) { 430 | case self::OPERATION_MAKE_ROOT: 431 | if ($this->treeAttribute === false) { 432 | throw new Exception('Can not move a node as the root when "treeAttribute" is false.'); 433 | } 434 | 435 | if ($this->owner->isRoot()) { 436 | throw new Exception('Can not move the root node as the root.'); 437 | } 438 | 439 | break; 440 | case self::OPERATION_INSERT_BEFORE: 441 | case self::OPERATION_INSERT_AFTER: 442 | if ($this->node->isRoot()) { 443 | throw new Exception('Can not move a node when the target node is root.'); 444 | } 445 | case self::OPERATION_PREPEND_TO: 446 | case self::OPERATION_APPEND_TO: 447 | if ($this->node->getIsNewRecord()) { 448 | throw new Exception('Can not move a node when the target node is new record.'); 449 | } 450 | 451 | if ($this->owner->equals($this->node)) { 452 | throw new Exception('Can not move a node when the target node is same.'); 453 | } 454 | 455 | if ($this->node->isChildOf($this->owner)) { 456 | throw new Exception('Can not move a node when the target node is child.'); 457 | } 458 | } 459 | } 460 | 461 | /** 462 | * @return void 463 | */ 464 | public function afterUpdate() 465 | { 466 | switch ($this->operation) { 467 | case self::OPERATION_MAKE_ROOT: 468 | $this->moveNodeAsRoot(); 469 | break; 470 | case self::OPERATION_PREPEND_TO: 471 | $this->moveNode($this->node->getAttribute($this->leftAttribute) + 1, 1); 472 | break; 473 | case self::OPERATION_APPEND_TO: 474 | $this->moveNode($this->node->getAttribute($this->rightAttribute), 1); 475 | break; 476 | case self::OPERATION_INSERT_BEFORE: 477 | $this->moveNode($this->node->getAttribute($this->leftAttribute), 0); 478 | break; 479 | case self::OPERATION_INSERT_AFTER: 480 | $this->moveNode($this->node->getAttribute($this->rightAttribute) + 1, 0); 481 | break; 482 | default: 483 | return; 484 | } 485 | 486 | $this->operation = null; 487 | $this->node = null; 488 | } 489 | 490 | /** 491 | * @return void 492 | */ 493 | protected function moveNodeAsRoot() 494 | { 495 | $db = $this->owner->getDb(); 496 | $leftValue = $this->owner->getAttribute($this->leftAttribute); 497 | $rightValue = $this->owner->getAttribute($this->rightAttribute); 498 | $depthValue = $this->owner->getAttribute($this->depthAttribute); 499 | $treeValue = $this->owner->getAttribute($this->treeAttribute); 500 | $leftAttribute = $db->quoteColumnName($this->leftAttribute); 501 | $rightAttribute = $db->quoteColumnName($this->rightAttribute); 502 | $depthAttribute = $db->quoteColumnName($this->depthAttribute); 503 | 504 | $this->owner->updateAll( 505 | [ 506 | $this->leftAttribute => new Expression($leftAttribute . sprintf('%+d', 1 - $leftValue)), 507 | $this->rightAttribute => new Expression($rightAttribute . sprintf('%+d', 1 - $leftValue)), 508 | $this->depthAttribute => new Expression($depthAttribute . sprintf('%+d', -$depthValue)), 509 | $this->treeAttribute => $this->owner->getPrimaryKey(), 510 | ], 511 | [ 512 | 'and', 513 | ['>=', $this->leftAttribute, $leftValue], 514 | ['<=', $this->rightAttribute, $rightValue], 515 | [$this->treeAttribute => $treeValue] 516 | ] 517 | ); 518 | 519 | $this->shiftLeftRightAttribute($rightValue + 1, $leftValue - $rightValue - 1); 520 | } 521 | 522 | /** 523 | * @param integer $value 524 | * @param integer $depth 525 | */ 526 | protected function moveNode($value, $depth) 527 | { 528 | $db = $this->owner->getDb(); 529 | $leftValue = $this->owner->getAttribute($this->leftAttribute); 530 | $rightValue = $this->owner->getAttribute($this->rightAttribute); 531 | $depthValue = $this->owner->getAttribute($this->depthAttribute); 532 | $depthAttribute = $db->quoteColumnName($this->depthAttribute); 533 | $depth = $this->node->getAttribute($this->depthAttribute) - $depthValue + $depth; 534 | 535 | if ($this->treeAttribute === false 536 | || $this->owner->getAttribute($this->treeAttribute) === $this->node->getAttribute($this->treeAttribute)) { 537 | $delta = $rightValue - $leftValue + 1; 538 | $this->shiftLeftRightAttribute($value, $delta); 539 | 540 | if ($leftValue >= $value) { 541 | $leftValue += $delta; 542 | $rightValue += $delta; 543 | } 544 | 545 | $condition = ['and', ['>=', $this->leftAttribute, $leftValue], ['<=', $this->rightAttribute, $rightValue]]; 546 | $this->applyTreeAttributeCondition($condition); 547 | 548 | $this->owner->updateAll( 549 | [$this->depthAttribute => new Expression($depthAttribute . sprintf('%+d', $depth))], 550 | $condition 551 | ); 552 | 553 | foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { 554 | $condition = ['and', ['>=', $attribute, $leftValue], ['<=', $attribute, $rightValue]]; 555 | $this->applyTreeAttributeCondition($condition); 556 | 557 | $this->owner->updateAll( 558 | [$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $value - $leftValue))], 559 | $condition 560 | ); 561 | } 562 | 563 | $this->shiftLeftRightAttribute($rightValue + 1, -$delta); 564 | } else { 565 | $leftAttribute = $db->quoteColumnName($this->leftAttribute); 566 | $rightAttribute = $db->quoteColumnName($this->rightAttribute); 567 | $nodeRootValue = $this->node->getAttribute($this->treeAttribute); 568 | 569 | foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { 570 | $this->owner->updateAll( 571 | [$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $rightValue - $leftValue + 1))], 572 | ['and', ['>=', $attribute, $value], [$this->treeAttribute => $nodeRootValue]] 573 | ); 574 | } 575 | 576 | $delta = $value - $leftValue; 577 | 578 | $this->owner->updateAll( 579 | [ 580 | $this->leftAttribute => new Expression($leftAttribute . sprintf('%+d', $delta)), 581 | $this->rightAttribute => new Expression($rightAttribute . sprintf('%+d', $delta)), 582 | $this->depthAttribute => new Expression($depthAttribute . sprintf('%+d', $depth)), 583 | $this->treeAttribute => $nodeRootValue, 584 | ], 585 | [ 586 | 'and', 587 | ['>=', $this->leftAttribute, $leftValue], 588 | ['<=', $this->rightAttribute, $rightValue], 589 | [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)], 590 | ] 591 | ); 592 | 593 | $this->shiftLeftRightAttribute($rightValue + 1, $leftValue - $rightValue - 1); 594 | } 595 | } 596 | 597 | /** 598 | * @throws Exception 599 | * @throws NotSupportedException 600 | */ 601 | public function beforeDelete() 602 | { 603 | if ($this->owner->getIsNewRecord()) { 604 | throw new Exception('Can not delete a node when it is new record.'); 605 | } 606 | 607 | if ($this->owner->isRoot() && $this->operation !== self::OPERATION_DELETE_WITH_CHILDREN) { 608 | throw new NotSupportedException('Method "'. get_class($this->owner) . '::delete" is not supported for deleting root nodes.'); 609 | } 610 | 611 | $this->owner->refresh(); 612 | } 613 | 614 | /** 615 | * @return void 616 | */ 617 | public function afterDelete() 618 | { 619 | $leftValue = $this->owner->getAttribute($this->leftAttribute); 620 | $rightValue = $this->owner->getAttribute($this->rightAttribute); 621 | 622 | if ($this->owner->isLeaf() || $this->operation === self::OPERATION_DELETE_WITH_CHILDREN) { 623 | $this->shiftLeftRightAttribute($rightValue + 1, $leftValue - $rightValue - 1); 624 | } else { 625 | $condition = [ 626 | 'and', 627 | ['>=', $this->leftAttribute, $this->owner->getAttribute($this->leftAttribute)], 628 | ['<=', $this->rightAttribute, $this->owner->getAttribute($this->rightAttribute)] 629 | ]; 630 | 631 | $this->applyTreeAttributeCondition($condition); 632 | $db = $this->owner->getDb(); 633 | 634 | $this->owner->updateAll( 635 | [ 636 | $this->leftAttribute => new Expression($db->quoteColumnName($this->leftAttribute) . sprintf('%+d', -1)), 637 | $this->rightAttribute => new Expression($db->quoteColumnName($this->rightAttribute) . sprintf('%+d', -1)), 638 | $this->depthAttribute => new Expression($db->quoteColumnName($this->depthAttribute) . sprintf('%+d', -1)), 639 | ], 640 | $condition 641 | ); 642 | 643 | $this->shiftLeftRightAttribute($rightValue + 1, -2); 644 | } 645 | 646 | $this->operation = null; 647 | $this->node = null; 648 | } 649 | 650 | /** 651 | * @param integer $value 652 | * @param integer $delta 653 | */ 654 | protected function shiftLeftRightAttribute($value, $delta) 655 | { 656 | $db = $this->owner->getDb(); 657 | 658 | foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) { 659 | $condition = ['>=', $attribute, $value]; 660 | $this->applyTreeAttributeCondition($condition); 661 | 662 | $this->owner->updateAll( 663 | [$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $delta))], 664 | $condition 665 | ); 666 | } 667 | } 668 | 669 | /** 670 | * @param array $condition 671 | */ 672 | protected function applyTreeAttributeCondition(&$condition) 673 | { 674 | if ($this->treeAttribute !== false) { 675 | $condition = [ 676 | 'and', 677 | $condition, 678 | [$this->treeAttribute => $this->owner->getAttribute($this->treeAttribute)] 679 | ]; 680 | } 681 | 682 | } 683 | } 684 | -------------------------------------------------------------------------------- /src/NestedSetsQueryBehavior.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class NestedSetsQueryBehavior extends Behavior 21 | { 22 | /** 23 | * Gets the root nodes. 24 | * @return \yii\db\ActiveQuery the owner 25 | */ 26 | public function roots() 27 | { 28 | $model = new $this->owner->modelClass(); 29 | 30 | $this->owner 31 | ->andWhere([$model->leftAttribute => 1]) 32 | ->addOrderBy([$model->primaryKey()[0] => SORT_ASC]); 33 | 34 | return $this->owner; 35 | } 36 | 37 | /** 38 | * Gets the leaf nodes. 39 | * @return \yii\db\ActiveQuery the owner 40 | */ 41 | public function leaves() 42 | { 43 | $model = new $this->owner->modelClass(); 44 | $db = $model->getDb(); 45 | 46 | $columns = [$model->leftAttribute => SORT_ASC]; 47 | 48 | if ($model->treeAttribute !== false) { 49 | $columns = [$model->treeAttribute => SORT_ASC] + $columns; 50 | } 51 | 52 | $this->owner 53 | ->andWhere([$model->rightAttribute => new Expression($db->quoteColumnName($model->leftAttribute) . '+ 1')]) 54 | ->addOrderBy($columns); 55 | 56 | return $this->owner; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/DatabaseTestCase.php: -------------------------------------------------------------------------------- 1 | createDefaultDBConnection(Yii::$app->getDb()->pdo); 23 | } 24 | 25 | /** 26 | * @inheritdoc 27 | */ 28 | public function getDataSet() 29 | { 30 | return $this->createFlatXMLDataSet(__DIR__ . '/data/test.xml'); 31 | } 32 | 33 | /** 34 | * @inheritdoc 35 | */ 36 | protected function setUp() 37 | { 38 | if (Yii::$app->get('db', false) === null) { 39 | $this->markTestSkipped(); 40 | } else { 41 | parent::setUp(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/MysqlNestedSetsBehaviorTest.php: -------------------------------------------------------------------------------- 1 | set('db', [ 25 | 'class' => Connection::className(), 26 | 'dsn' => 'mysql:host=localhost;dbname=yii2_nested_sets_test', 27 | 'username' => 'root', 28 | 'password' => '', 29 | ]); 30 | 31 | Yii::$app->getDb()->open(); 32 | $lines = explode(';', file_get_contents(__DIR__ . '/migrations/mysql.sql')); 33 | 34 | foreach ($lines as $line) { 35 | if (trim($line) !== '') { 36 | Yii::$app->getDb()->pdo->exec($line); 37 | } 38 | } 39 | } catch (\Exception $e) { 40 | Yii::$app->clear('db'); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/MysqlNestedSetsQueryBehaviorTest.php: -------------------------------------------------------------------------------- 1 | set('db', [ 25 | 'class' => Connection::className(), 26 | 'dsn' => 'mysql:host=localhost;dbname=yii2_nested_sets_test', 27 | 'username' => 'root', 28 | 'password' => '', 29 | ]); 30 | 31 | Yii::$app->getDb()->open(); 32 | $lines = explode(';', file_get_contents(__DIR__ . '/migrations/mysql.sql')); 33 | 34 | foreach ($lines as $line) { 35 | if (trim($line) !== '') { 36 | Yii::$app->getDb()->pdo->exec($line); 37 | } 38 | } 39 | } catch (\Exception $e) { 40 | Yii::$app->clear('db'); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/NestedSetsBehaviorTest.php: -------------------------------------------------------------------------------- 1 | createFlatXMLDataSet(__DIR__ . '/data/clean.xml'); 24 | $this->getDatabaseTester()->setDataSet($dataSet); 25 | $this->getDatabaseTester()->onSetUp(); 26 | 27 | $node = new Tree(['name' => 'Root']); 28 | $this->assertTrue($node->makeRoot()); 29 | 30 | $node = new MultipleTree(['name' => 'Root 1']); 31 | $this->assertTrue($node->makeRoot()); 32 | 33 | $node = new MultipleTree(['name' => 'Root 2']); 34 | $this->assertTrue($node->makeRoot()); 35 | 36 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 37 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-make-root-new.xml'); 38 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 39 | } 40 | 41 | /** 42 | * @expectedException \yii\db\Exception 43 | */ 44 | public function testMakeRootNewExceptionIsRaisedWhenTreeAttributeIsFalseAndRootIsExists() 45 | { 46 | $node = new Tree(['name' => 'Root']); 47 | $node->makeRoot(); 48 | } 49 | 50 | public function testPrependToNew() 51 | { 52 | $node = new Tree(['name' => 'New node']); 53 | $this->assertTrue($node->prependTo(Tree::findOne(9))); 54 | 55 | $node = new MultipleTree(['name' => 'New node']); 56 | $this->assertTrue($node->prependTo(MultipleTree::findOne(31))); 57 | 58 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 59 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-prepend-to-new.xml'); 60 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 61 | } 62 | 63 | /** 64 | * @expectedException \yii\db\Exception 65 | */ 66 | public function testPrependToNewExceptionIsRaisedWhenTargetIsNewRecord() 67 | { 68 | $node = new Tree(['name' => 'New node']); 69 | $node->prependTo(new Tree()); 70 | } 71 | 72 | public function testAppendToNew() 73 | { 74 | $node = new Tree(['name' => 'New node']); 75 | $this->assertTrue($node->appendTo(Tree::findOne(9))); 76 | 77 | $node = new MultipleTree(['name' => 'New node']); 78 | $this->assertTrue($node->appendTo(MultipleTree::findOne(31))); 79 | 80 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 81 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-append-to-new.xml'); 82 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 83 | } 84 | 85 | /** 86 | * @expectedException \yii\db\Exception 87 | */ 88 | public function testAppendNewToExceptionIsRaisedWhenTargetIsNewRecord() 89 | { 90 | $node = new Tree(['name' => 'New node']); 91 | $node->appendTo(new Tree()); 92 | } 93 | 94 | public function testInsertBeforeNew() 95 | { 96 | $node = new Tree(['name' => 'New node']); 97 | $this->assertTrue($node->insertBefore(Tree::findOne(9))); 98 | 99 | $node = new MultipleTree(['name' => 'New node']); 100 | $this->assertTrue($node->insertBefore(MultipleTree::findOne(31))); 101 | 102 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 103 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-insert-before-new.xml'); 104 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 105 | } 106 | 107 | /** 108 | * @expectedException \yii\db\Exception 109 | */ 110 | public function testInsertBeforeNewExceptionIsRaisedWhenTargetIsNewRecord() 111 | { 112 | $node = new Tree(['name' => 'New node']); 113 | $node->insertBefore(new Tree()); 114 | } 115 | 116 | /** 117 | * @expectedException \yii\db\Exception 118 | */ 119 | public function testInsertBeforeNewExceptionIsRaisedWhenTargetIsRoot() 120 | { 121 | $node = new Tree(['name' => 'New node']); 122 | $node->insertBefore(Tree::findOne(1)); 123 | } 124 | 125 | public function testInsertAfterNew() 126 | { 127 | $node = new Tree(['name' => 'New node']); 128 | $this->assertTrue($node->insertAfter(Tree::findOne(9))); 129 | 130 | $node = new MultipleTree(['name' => 'New node']); 131 | $this->assertTrue($node->insertAfter(MultipleTree::findOne(31))); 132 | 133 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 134 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-insert-after-new.xml'); 135 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 136 | } 137 | 138 | /** 139 | * @expectedException \yii\db\Exception 140 | */ 141 | public function testInsertAfterNewExceptionIsRaisedWhenTargetIsNewRecord() 142 | { 143 | $node = new Tree(['name' => 'New node']); 144 | $node->insertAfter(new Tree()); 145 | } 146 | 147 | /** 148 | * @expectedException \yii\db\Exception 149 | */ 150 | public function testInsertAfterNewExceptionIsRaisedWhenTargetIsRoot() 151 | { 152 | $node = new Tree(['name' => 'New node']); 153 | $node->insertAfter(Tree::findOne(1)); 154 | } 155 | 156 | public function testMakeRootExists() 157 | { 158 | $node = MultipleTree::findOne(31); 159 | $node->name = 'Updated node 2'; 160 | $this->assertTrue($node->makeRoot()); 161 | 162 | $dataSet = $this->getConnection()->createDataSet(['multiple_tree']); 163 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-make-root-exists.xml'); 164 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 165 | } 166 | 167 | /** 168 | * @expectedException \yii\db\Exception 169 | */ 170 | public function testMakeRootExistsExceptionIsRaisedWhenTreeAttributeIsFalse() 171 | { 172 | $node = Tree::findOne(9); 173 | $node->makeRoot(); 174 | } 175 | 176 | /** 177 | * @expectedException \yii\db\Exception 178 | */ 179 | public function testMakeRootExistsExceptionIsRaisedWhenItsRoot() 180 | { 181 | $node = MultipleTree::findOne(23); 182 | $node->makeRoot(); 183 | } 184 | 185 | public function testPrependToExistsUp() 186 | { 187 | $node = Tree::findOne(9); 188 | $node->name = 'Updated node 2'; 189 | $this->assertTrue($node->prependTo(Tree::findOne(2))); 190 | 191 | $node = MultipleTree::findOne(31); 192 | $node->name = 'Updated node 2'; 193 | $this->assertTrue($node->prependTo(MultipleTree::findOne(24))); 194 | 195 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 196 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-prepend-to-exists-up.xml'); 197 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 198 | } 199 | 200 | public function testPrependToExistsDown() 201 | { 202 | $node = Tree::findOne(9); 203 | $node->name = 'Updated node 2'; 204 | $this->assertTrue($node->prependTo(Tree::findOne(16))); 205 | 206 | $node = MultipleTree::findOne(31); 207 | $node->name = 'Updated node 2'; 208 | $this->assertTrue($node->prependTo(MultipleTree::findOne(38))); 209 | 210 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 211 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-prepend-to-exists-down.xml'); 212 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 213 | } 214 | 215 | public function testPrependToExistsAnotherTree() 216 | { 217 | $node = MultipleTree::findOne(9); 218 | $node->name = 'Updated node 2'; 219 | $this->assertTrue($node->prependTo(MultipleTree::findOne(53))); 220 | 221 | $dataSet = $this->getConnection()->createDataSet(['multiple_tree']); 222 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-prepend-to-exists-another-tree.xml'); 223 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 224 | } 225 | 226 | /** 227 | * @expectedException \yii\db\Exception 228 | */ 229 | public function testPrependToExistsExceptionIsRaisedWhenTargetIsNewRecord() 230 | { 231 | $node = Tree::findOne(9); 232 | $node->prependTo(new Tree()); 233 | } 234 | 235 | /** 236 | * @expectedException \yii\db\Exception 237 | */ 238 | public function testPrependToExistsExceptionIsRaisedWhenTargetIsSame() 239 | { 240 | $node = Tree::findOne(9); 241 | $node->prependTo(Tree::findOne(9)); 242 | } 243 | 244 | /** 245 | * @expectedException \yii\db\Exception 246 | */ 247 | public function testPrependToExistsExceptionIsRaisedWhenTargetIsChild() 248 | { 249 | $node = Tree::findOne(9); 250 | $node->prependTo(Tree::findOne(11)); 251 | } 252 | 253 | public function testAppendToExistsUp() 254 | { 255 | $node = Tree::findOne(9); 256 | $node->name = 'Updated node 2'; 257 | $this->assertTrue($node->appendTo(Tree::findOne(2))); 258 | 259 | $node = MultipleTree::findOne(31); 260 | $node->name = 'Updated node 2'; 261 | $this->assertTrue($node->appendTo(MultipleTree::findOne(24))); 262 | 263 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 264 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-append-to-exists-up.xml'); 265 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 266 | } 267 | 268 | public function testAppendToExistsDown() 269 | { 270 | $node = Tree::findOne(9); 271 | $node->name = 'Updated node 2'; 272 | $this->assertTrue($node->appendTo(Tree::findOne(16))); 273 | 274 | $node = MultipleTree::findOne(31); 275 | $node->name = 'Updated node 2'; 276 | $this->assertTrue($node->appendTo(MultipleTree::findOne(38))); 277 | 278 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 279 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-append-to-exists-down.xml'); 280 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 281 | } 282 | 283 | public function testAppendToExistsAnotherTree() 284 | { 285 | $node = MultipleTree::findOne(9); 286 | $node->name = 'Updated node 2'; 287 | $this->assertTrue($node->appendTo(MultipleTree::findOne(53))); 288 | 289 | $dataSet = $this->getConnection()->createDataSet(['multiple_tree']); 290 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-append-to-exists-another-tree.xml'); 291 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 292 | } 293 | 294 | /** 295 | * @expectedException \yii\db\Exception 296 | */ 297 | public function testAppendToExistsExceptionIsRaisedWhenTargetIsNewRecord() 298 | { 299 | $node = Tree::findOne(9); 300 | $node->appendTo(new Tree()); 301 | } 302 | 303 | /** 304 | * @expectedException \yii\db\Exception 305 | */ 306 | public function testAppendToExistsExceptionIsRaisedWhenTargetIsSame() 307 | { 308 | $node = Tree::findOne(9); 309 | $node->appendTo(Tree::findOne(9)); 310 | } 311 | 312 | /** 313 | * @expectedException \yii\db\Exception 314 | */ 315 | public function testAppendToExistsExceptionIsRaisedWhenTargetIsChild() 316 | { 317 | $node = Tree::findOne(9); 318 | $node->appendTo(Tree::findOne(11)); 319 | } 320 | 321 | public function testInsertBeforeExistsUp() 322 | { 323 | $node = Tree::findOne(9); 324 | $node->name = 'Updated node 2'; 325 | $this->assertTrue($node->insertBefore(Tree::findOne(2))); 326 | 327 | $node = MultipleTree::findOne(31); 328 | $node->name = 'Updated node 2'; 329 | $this->assertTrue($node->insertBefore(MultipleTree::findOne(24))); 330 | 331 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 332 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-insert-before-exists-up.xml'); 333 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 334 | } 335 | 336 | public function testInsertBeforeExistsDown() 337 | { 338 | $node = Tree::findOne(9); 339 | $node->name = 'Updated node 2'; 340 | $this->assertTrue($node->insertBefore(Tree::findOne(16))); 341 | 342 | $node = MultipleTree::findOne(31); 343 | $node->name = 'Updated node 2'; 344 | $this->assertTrue($node->insertBefore(MultipleTree::findOne(38))); 345 | 346 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 347 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-insert-before-exists-down.xml'); 348 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 349 | } 350 | 351 | public function testInsertBeforeExistsAnotherTree() 352 | { 353 | $node = MultipleTree::findOne(9); 354 | $node->name = 'Updated node 2'; 355 | $this->assertTrue($node->insertBefore(MultipleTree::findOne(53))); 356 | 357 | $dataSet = $this->getConnection()->createDataSet(['multiple_tree']); 358 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-insert-before-exists-another-tree.xml'); 359 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 360 | } 361 | 362 | /** 363 | * @expectedException \yii\db\Exception 364 | */ 365 | public function testInsertBeforeExistsExceptionIsRaisedWhenTargetIsNewRecord() 366 | { 367 | $node = Tree::findOne(9); 368 | $node->insertBefore(new Tree()); 369 | } 370 | 371 | /** 372 | * @expectedException \yii\db\Exception 373 | */ 374 | public function testInsertBeforeExistsExceptionIsRaisedWhenTargetIsSame() 375 | { 376 | $node = Tree::findOne(9); 377 | $node->insertBefore(Tree::findOne(9)); 378 | } 379 | 380 | /** 381 | * @expectedException \yii\db\Exception 382 | */ 383 | public function testInsertBeforeExistsExceptionIsRaisedWhenTargetIsChild() 384 | { 385 | $node = Tree::findOne(9); 386 | $node->insertBefore(Tree::findOne(11)); 387 | } 388 | 389 | /** 390 | * @expectedException \yii\db\Exception 391 | */ 392 | public function testInsertBeforeExistsExceptionIsRaisedWhenTargetIsRoot() 393 | { 394 | $node = Tree::findOne(9); 395 | $node->insertBefore(Tree::findOne(1)); 396 | } 397 | 398 | public function testInsertAfterExistsUp() 399 | { 400 | $node = Tree::findOne(9); 401 | $node->name = 'Updated node 2'; 402 | $this->assertTrue($node->insertAfter(Tree::findOne(2))); 403 | 404 | $node = MultipleTree::findOne(31); 405 | $node->name = 'Updated node 2'; 406 | $this->assertTrue($node->insertAfter(MultipleTree::findOne(24))); 407 | 408 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 409 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-insert-after-exists-up.xml'); 410 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 411 | } 412 | 413 | public function testInsertAfterExistsDown() 414 | { 415 | $node = Tree::findOne(9); 416 | $node->name = 'Updated node 2'; 417 | $this->assertTrue($node->insertAfter(Tree::findOne(16))); 418 | 419 | $node = MultipleTree::findOne(31); 420 | $node->name = 'Updated node 2'; 421 | $this->assertTrue($node->insertAfter(MultipleTree::findOne(38))); 422 | 423 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 424 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-insert-after-exists-down.xml'); 425 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 426 | } 427 | 428 | public function testInsertAfterExistsAnotherTree() 429 | { 430 | $node = MultipleTree::findOne(9); 431 | $node->name = 'Updated node 2'; 432 | $this->assertTrue($node->insertAfter(MultipleTree::findOne(53))); 433 | 434 | $dataSet = $this->getConnection()->createDataSet(['multiple_tree']); 435 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-insert-after-exists-another-tree.xml'); 436 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 437 | } 438 | 439 | /** 440 | * @expectedException \yii\db\Exception 441 | */ 442 | public function testInsertAfterExistsExceptionIsRaisedWhenTargetIsNewRecord() 443 | { 444 | $node = Tree::findOne(9); 445 | $node->insertAfter(new Tree()); 446 | } 447 | 448 | /** 449 | * @expectedException \yii\db\Exception 450 | */ 451 | public function testInsertAfterExistsExceptionIsRaisedWhenTargetIsSame() 452 | { 453 | $node = Tree::findOne(9); 454 | $node->insertAfter(Tree::findOne(9)); 455 | } 456 | 457 | /** 458 | * @expectedException \yii\db\Exception 459 | */ 460 | public function testInsertAfterExistsExceptionIsRaisedWhenTargetIsChild() 461 | { 462 | $node = Tree::findOne(9); 463 | $node->insertAfter(Tree::findOne(11)); 464 | } 465 | 466 | /** 467 | * @expectedException \yii\db\Exception 468 | */ 469 | public function testInsertAfterExistsExceptionIsRaisedWhenTargetIsRoot() 470 | { 471 | $node = Tree::findOne(9); 472 | $node->insertAfter(Tree::findOne(1)); 473 | } 474 | 475 | public function testDeleteWithChildren() 476 | { 477 | $this->assertEquals(7, Tree::findOne(9)->deleteWithChildren()); 478 | $this->assertEquals(7, MultipleTree::findOne(31)->deleteWithChildren()); 479 | 480 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 481 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-delete-with-children.xml'); 482 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 483 | } 484 | 485 | /** 486 | * @expectedException \yii\db\Exception 487 | */ 488 | public function testDeleteWithChildrenExceptionIsRaisedWhenNodeIsNewRecord() 489 | { 490 | $node = new Tree(); 491 | $node->deleteWithChildren(); 492 | } 493 | 494 | public function testDelete() 495 | { 496 | $this->assertEquals(1, Tree::findOne(9)->delete()); 497 | $this->assertEquals(1, MultipleTree::findOne(31)->delete()); 498 | 499 | $dataSet = $this->getConnection()->createDataSet(['tree', 'multiple_tree']); 500 | $expectedDataSet = $this->createFlatXMLDataSet(__DIR__ . '/data/test-delete.xml'); 501 | $this->assertDataSetsEqual($expectedDataSet, $dataSet); 502 | } 503 | 504 | /** 505 | * @expectedException \yii\db\Exception 506 | */ 507 | public function testDeleteExceptionIsRaisedWhenNodeIsNewRecord() 508 | { 509 | $node = new Tree(); 510 | $node->delete(); 511 | } 512 | 513 | /** 514 | * @expectedException \yii\base\NotSupportedException 515 | */ 516 | public function testDeleteExceptionIsRaisedWhenNodeIsRoot() 517 | { 518 | $node = Tree::findOne(1); 519 | $node->delete(); 520 | } 521 | 522 | /** 523 | * @expectedException \yii\base\NotSupportedException 524 | */ 525 | public function testExceptionIsRaisedWhenInsertIsCalled() 526 | { 527 | $node = new Tree(['name' => 'Node']); 528 | $node->insert(); 529 | } 530 | 531 | public function testUpdate() 532 | { 533 | $node = Tree::findOne(9); 534 | $node->name = 'Updated node 2'; 535 | $this->assertEquals(1, $node->update()); 536 | } 537 | 538 | public function testParents() 539 | { 540 | $this->assertEquals( 541 | require(__DIR__ . '/data/test-parents.php'), 542 | ArrayHelper::toArray(Tree::findOne(11)->parents()->all()) 543 | ); 544 | 545 | $this->assertEquals( 546 | require(__DIR__ . '/data/test-parents-multiple-tree.php'), 547 | ArrayHelper::toArray(MultipleTree::findOne(33)->parents()->all()) 548 | ); 549 | 550 | $this->assertEquals( 551 | require(__DIR__ . '/data/test-parents-with-depth.php'), 552 | ArrayHelper::toArray(Tree::findOne(11)->parents(1)->all()) 553 | ); 554 | 555 | $this->assertEquals( 556 | require(__DIR__ . '/data/test-parents-multiple-tree-with-depth.php'), 557 | ArrayHelper::toArray(MultipleTree::findOne(33)->parents(1)->all()) 558 | ); 559 | } 560 | 561 | public function testChildren() 562 | { 563 | $this->assertEquals( 564 | require(__DIR__ . '/data/test-children.php'), 565 | ArrayHelper::toArray(Tree::findOne(9)->children()->all()) 566 | ); 567 | 568 | $this->assertEquals( 569 | require(__DIR__ . '/data/test-children-multiple-tree.php'), 570 | ArrayHelper::toArray(MultipleTree::findOne(31)->children()->all()) 571 | ); 572 | 573 | $this->assertEquals( 574 | require(__DIR__ . '/data/test-children-with-depth.php'), 575 | ArrayHelper::toArray(Tree::findOne(9)->children(1)->all()) 576 | ); 577 | 578 | $this->assertEquals( 579 | require(__DIR__ . '/data/test-children-multiple-tree-with-depth.php'), 580 | ArrayHelper::toArray(MultipleTree::findOne(31)->children(1)->all()) 581 | ); 582 | } 583 | 584 | public function testLeaves() 585 | { 586 | $this->assertEquals( 587 | require(__DIR__ . '/data/test-leaves.php'), 588 | ArrayHelper::toArray(Tree::findOne(9)->leaves()->all()) 589 | ); 590 | 591 | $this->assertEquals( 592 | require(__DIR__ . '/data/test-leaves-multiple-tree.php'), 593 | ArrayHelper::toArray(MultipleTree::findOne(31)->leaves()->all()) 594 | ); 595 | } 596 | 597 | public function testPrev() 598 | { 599 | $this->assertEquals( 600 | require(__DIR__ . '/data/test-prev.php'), 601 | ArrayHelper::toArray(Tree::findOne(9)->prev()->all()) 602 | ); 603 | 604 | $this->assertEquals( 605 | require(__DIR__ . '/data/test-prev-multiple-tree.php'), 606 | ArrayHelper::toArray(MultipleTree::findOne(31)->prev()->all()) 607 | ); 608 | } 609 | 610 | public function testNext() 611 | { 612 | $this->assertEquals( 613 | require(__DIR__ . '/data/test-next.php'), 614 | ArrayHelper::toArray(Tree::findOne(9)->next()->all()) 615 | ); 616 | 617 | $this->assertEquals( 618 | require(__DIR__ . '/data/test-next-multiple-tree.php'), 619 | ArrayHelper::toArray(MultipleTree::findOne(31)->next()->all()) 620 | ); 621 | } 622 | 623 | public function testIsRoot() 624 | { 625 | $this->assertTrue(Tree::findOne(1)->isRoot()); 626 | $this->assertFalse(Tree::findOne(2)->isRoot()); 627 | } 628 | 629 | public function testIsChildOf() 630 | { 631 | $node = MultipleTree::findOne(26); 632 | $this->assertTrue($node->isChildOf(MultipleTree::findOne(25))); 633 | $this->assertTrue($node->isChildOf(MultipleTree::findOne(23))); 634 | $this->assertFalse($node->isChildOf(MultipleTree::findOne(3))); 635 | $this->assertFalse($node->isChildOf(MultipleTree::findOne(1))); 636 | } 637 | 638 | public function testIsLeaf() 639 | { 640 | $this->assertTrue(Tree::findOne(4)->isLeaf()); 641 | $this->assertFalse(Tree::findOne(1)->isLeaf()); 642 | } 643 | 644 | /** 645 | * @inheritdoc 646 | */ 647 | public static function setUpBeforeClass() 648 | { 649 | try { 650 | Yii::$app->set('db', [ 651 | 'class' => Connection::className(), 652 | 'dsn' => 'sqlite::memory:', 653 | ]); 654 | 655 | Yii::$app->getDb()->open(); 656 | $lines = explode(';', file_get_contents(__DIR__ . '/migrations/sqlite.sql')); 657 | 658 | foreach ($lines as $line) { 659 | if (trim($line) !== '') { 660 | Yii::$app->getDb()->pdo->exec($line); 661 | } 662 | } 663 | } catch (\Exception $e) { 664 | Yii::$app->clear('db'); 665 | } 666 | } 667 | } 668 | -------------------------------------------------------------------------------- /tests/NestedSetsQueryBehaviorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 24 | require(__DIR__ . '/data/test-roots-query.php'), 25 | ArrayHelper::toArray(Tree::find()->roots()->all()) 26 | ); 27 | 28 | $this->assertEquals( 29 | require(__DIR__ . '/data/test-roots-multiple-tree-query.php'), 30 | ArrayHelper::toArray(MultipleTree::find()->roots()->all()) 31 | ); 32 | } 33 | 34 | public function testLeaves() 35 | { 36 | $this->assertEquals( 37 | require(__DIR__ . '/data/test-leaves-query.php'), 38 | ArrayHelper::toArray(Tree::find()->leaves()->all()) 39 | ); 40 | 41 | $this->assertEquals( 42 | require(__DIR__ . '/data/test-leaves-multiple-tree-query.php'), 43 | ArrayHelper::toArray(MultipleTree::find()->leaves()->all()) 44 | ); 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public static function setUpBeforeClass() 51 | { 52 | try { 53 | Yii::$app->set('db', [ 54 | 'class' => Connection::className(), 55 | 'dsn' => 'sqlite::memory:', 56 | ]); 57 | 58 | Yii::$app->getDb()->open(); 59 | $lines = explode(';', file_get_contents(__DIR__ . '/migrations/sqlite.sql')); 60 | 61 | foreach ($lines as $line) { 62 | if (trim($line) !== '') { 63 | Yii::$app->getDb()->pdo->exec($line); 64 | } 65 | } 66 | } catch (\Exception $e) { 67 | Yii::$app->clear('db'); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'unit', 13 | 'basePath' => __DIR__, 14 | ]); 15 | -------------------------------------------------------------------------------- /tests/data/clean.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/data/test-append-to-exists-another-tree.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/data/test-append-to-exists-down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/data/test-append-to-exists-up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/data/test-append-to-new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /tests/data/test-children-multiple-tree-with-depth.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 32, 7 | 'tree' => 23, 8 | 'lft' => 17, 9 | 'rgt' => 22, 10 | 'depth' => 2, 11 | 'name' => 'Node 2.1', 12 | ), 13 | 1 => 14 | array ( 15 | 'id' => 35, 16 | 'tree' => 23, 17 | 'lft' => 23, 18 | 'rgt' => 28, 19 | 'depth' => 2, 20 | 'name' => 'Node 2.2', 21 | ), 22 | ); -------------------------------------------------------------------------------- /tests/data/test-children-multiple-tree.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 32, 7 | 'tree' => 23, 8 | 'lft' => 17, 9 | 'rgt' => 22, 10 | 'depth' => 2, 11 | 'name' => 'Node 2.1', 12 | ), 13 | 1 => 14 | array ( 15 | 'id' => 33, 16 | 'tree' => 23, 17 | 'lft' => 18, 18 | 'rgt' => 19, 19 | 'depth' => 3, 20 | 'name' => 'Node 2.1.1', 21 | ), 22 | 2 => 23 | array ( 24 | 'id' => 34, 25 | 'tree' => 23, 26 | 'lft' => 20, 27 | 'rgt' => 21, 28 | 'depth' => 3, 29 | 'name' => 'Node 2.1.2', 30 | ), 31 | 3 => 32 | array ( 33 | 'id' => 35, 34 | 'tree' => 23, 35 | 'lft' => 23, 36 | 'rgt' => 28, 37 | 'depth' => 2, 38 | 'name' => 'Node 2.2', 39 | ), 40 | 4 => 41 | array ( 42 | 'id' => 36, 43 | 'tree' => 23, 44 | 'lft' => 24, 45 | 'rgt' => 25, 46 | 'depth' => 3, 47 | 'name' => 'Node 2.2.1', 48 | ), 49 | 5 => 50 | array ( 51 | 'id' => 37, 52 | 'tree' => 23, 53 | 'lft' => 26, 54 | 'rgt' => 27, 55 | 'depth' => 3, 56 | 'name' => 'Node 2.2.2', 57 | ), 58 | ); -------------------------------------------------------------------------------- /tests/data/test-children-with-depth.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 10, 7 | 'lft' => 17, 8 | 'rgt' => 22, 9 | 'depth' => 2, 10 | 'name' => 'Node 2.1', 11 | ), 12 | 1 => 13 | array ( 14 | 'id' => 13, 15 | 'lft' => 23, 16 | 'rgt' => 28, 17 | 'depth' => 2, 18 | 'name' => 'Node 2.2', 19 | ), 20 | ); -------------------------------------------------------------------------------- /tests/data/test-children.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 10, 7 | 'lft' => 17, 8 | 'rgt' => 22, 9 | 'depth' => 2, 10 | 'name' => 'Node 2.1', 11 | ), 12 | 1 => 13 | array ( 14 | 'id' => 11, 15 | 'lft' => 18, 16 | 'rgt' => 19, 17 | 'depth' => 3, 18 | 'name' => 'Node 2.1.1', 19 | ), 20 | 2 => 21 | array ( 22 | 'id' => 12, 23 | 'lft' => 20, 24 | 'rgt' => 21, 25 | 'depth' => 3, 26 | 'name' => 'Node 2.1.2', 27 | ), 28 | 3 => 29 | array ( 30 | 'id' => 13, 31 | 'lft' => 23, 32 | 'rgt' => 28, 33 | 'depth' => 2, 34 | 'name' => 'Node 2.2', 35 | ), 36 | 4 => 37 | array ( 38 | 'id' => 14, 39 | 'lft' => 24, 40 | 'rgt' => 25, 41 | 'depth' => 3, 42 | 'name' => 'Node 2.2.1', 43 | ), 44 | 5 => 45 | array ( 46 | 'id' => 15, 47 | 'lft' => 26, 48 | 'rgt' => 27, 49 | 'depth' => 3, 50 | 'name' => 'Node 2.2.2', 51 | ), 52 | ); -------------------------------------------------------------------------------- /tests/data/test-delete-with-children.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /tests/data/test-delete.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /tests/data/test-insert-after-exists-another-tree.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/data/test-insert-after-exists-down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/data/test-insert-after-exists-up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/data/test-insert-after-new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /tests/data/test-insert-before-exists-another-tree.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/data/test-insert-before-exists-down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/data/test-insert-before-exists-up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/data/test-insert-before-new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /tests/data/test-leaves-multiple-tree-query.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 4, 7 | 'tree' => 1, 8 | 'lft' => 4, 9 | 'rgt' => 5, 10 | 'depth' => 3, 11 | 'name' => 'Node 1.1.1', 12 | ), 13 | 1 => 14 | array ( 15 | 'id' => 5, 16 | 'tree' => 1, 17 | 'lft' => 6, 18 | 'rgt' => 7, 19 | 'depth' => 3, 20 | 'name' => 'Node 1.1.2', 21 | ), 22 | 2 => 23 | array ( 24 | 'id' => 7, 25 | 'tree' => 1, 26 | 'lft' => 10, 27 | 'rgt' => 11, 28 | 'depth' => 3, 29 | 'name' => 'Node 1.2.1', 30 | ), 31 | 3 => 32 | array ( 33 | 'id' => 8, 34 | 'tree' => 1, 35 | 'lft' => 12, 36 | 'rgt' => 13, 37 | 'depth' => 3, 38 | 'name' => 'Node 1.2.2', 39 | ), 40 | 4 => 41 | array ( 42 | 'id' => 11, 43 | 'tree' => 1, 44 | 'lft' => 18, 45 | 'rgt' => 19, 46 | 'depth' => 3, 47 | 'name' => 'Node 2.1.1', 48 | ), 49 | 5 => 50 | array ( 51 | 'id' => 12, 52 | 'tree' => 1, 53 | 'lft' => 20, 54 | 'rgt' => 21, 55 | 'depth' => 3, 56 | 'name' => 'Node 2.1.2', 57 | ), 58 | 6 => 59 | array ( 60 | 'id' => 14, 61 | 'tree' => 1, 62 | 'lft' => 24, 63 | 'rgt' => 25, 64 | 'depth' => 3, 65 | 'name' => 'Node 2.2.1', 66 | ), 67 | 7 => 68 | array ( 69 | 'id' => 15, 70 | 'tree' => 1, 71 | 'lft' => 26, 72 | 'rgt' => 27, 73 | 'depth' => 3, 74 | 'name' => 'Node 2.2.2', 75 | ), 76 | 8 => 77 | array ( 78 | 'id' => 18, 79 | 'tree' => 1, 80 | 'lft' => 32, 81 | 'rgt' => 33, 82 | 'depth' => 3, 83 | 'name' => 'Node 3.1.1', 84 | ), 85 | 9 => 86 | array ( 87 | 'id' => 19, 88 | 'tree' => 1, 89 | 'lft' => 34, 90 | 'rgt' => 35, 91 | 'depth' => 3, 92 | 'name' => 'Node 3.1.2', 93 | ), 94 | 10 => 95 | array ( 96 | 'id' => 21, 97 | 'tree' => 1, 98 | 'lft' => 38, 99 | 'rgt' => 39, 100 | 'depth' => 3, 101 | 'name' => 'Node 3.2.1', 102 | ), 103 | 11 => 104 | array ( 105 | 'id' => 22, 106 | 'tree' => 1, 107 | 'lft' => 40, 108 | 'rgt' => 41, 109 | 'depth' => 3, 110 | 'name' => 'Node 3.2.2', 111 | ), 112 | 12 => 113 | array ( 114 | 'id' => 26, 115 | 'tree' => 23, 116 | 'lft' => 4, 117 | 'rgt' => 5, 118 | 'depth' => 3, 119 | 'name' => 'Node 1.1.1', 120 | ), 121 | 13 => 122 | array ( 123 | 'id' => 27, 124 | 'tree' => 23, 125 | 'lft' => 6, 126 | 'rgt' => 7, 127 | 'depth' => 3, 128 | 'name' => 'Node 1.1.2', 129 | ), 130 | 14 => 131 | array ( 132 | 'id' => 29, 133 | 'tree' => 23, 134 | 'lft' => 10, 135 | 'rgt' => 11, 136 | 'depth' => 3, 137 | 'name' => 'Node 1.2.1', 138 | ), 139 | 15 => 140 | array ( 141 | 'id' => 30, 142 | 'tree' => 23, 143 | 'lft' => 12, 144 | 'rgt' => 13, 145 | 'depth' => 3, 146 | 'name' => 'Node 1.2.2', 147 | ), 148 | 16 => 149 | array ( 150 | 'id' => 33, 151 | 'tree' => 23, 152 | 'lft' => 18, 153 | 'rgt' => 19, 154 | 'depth' => 3, 155 | 'name' => 'Node 2.1.1', 156 | ), 157 | 17 => 158 | array ( 159 | 'id' => 34, 160 | 'tree' => 23, 161 | 'lft' => 20, 162 | 'rgt' => 21, 163 | 'depth' => 3, 164 | 'name' => 'Node 2.1.2', 165 | ), 166 | 18 => 167 | array ( 168 | 'id' => 36, 169 | 'tree' => 23, 170 | 'lft' => 24, 171 | 'rgt' => 25, 172 | 'depth' => 3, 173 | 'name' => 'Node 2.2.1', 174 | ), 175 | 19 => 176 | array ( 177 | 'id' => 37, 178 | 'tree' => 23, 179 | 'lft' => 26, 180 | 'rgt' => 27, 181 | 'depth' => 3, 182 | 'name' => 'Node 2.2.2', 183 | ), 184 | 20 => 185 | array ( 186 | 'id' => 40, 187 | 'tree' => 23, 188 | 'lft' => 32, 189 | 'rgt' => 33, 190 | 'depth' => 3, 191 | 'name' => 'Node 3.1.1', 192 | ), 193 | 21 => 194 | array ( 195 | 'id' => 41, 196 | 'tree' => 23, 197 | 'lft' => 34, 198 | 'rgt' => 35, 199 | 'depth' => 3, 200 | 'name' => 'Node 3.1.2', 201 | ), 202 | 22 => 203 | array ( 204 | 'id' => 43, 205 | 'tree' => 23, 206 | 'lft' => 38, 207 | 'rgt' => 39, 208 | 'depth' => 3, 209 | 'name' => 'Node 3.2.1', 210 | ), 211 | 23 => 212 | array ( 213 | 'id' => 44, 214 | 'tree' => 23, 215 | 'lft' => 40, 216 | 'rgt' => 41, 217 | 'depth' => 3, 218 | 'name' => 'Node 3.2.2', 219 | ), 220 | 24 => 221 | array ( 222 | 'id' => 48, 223 | 'tree' => 45, 224 | 'lft' => 4, 225 | 'rgt' => 5, 226 | 'depth' => 3, 227 | 'name' => 'Node 1.1.1', 228 | ), 229 | 25 => 230 | array ( 231 | 'id' => 49, 232 | 'tree' => 45, 233 | 'lft' => 6, 234 | 'rgt' => 7, 235 | 'depth' => 3, 236 | 'name' => 'Node 1.1.2', 237 | ), 238 | 26 => 239 | array ( 240 | 'id' => 51, 241 | 'tree' => 45, 242 | 'lft' => 10, 243 | 'rgt' => 11, 244 | 'depth' => 3, 245 | 'name' => 'Node 1.2.1', 246 | ), 247 | 27 => 248 | array ( 249 | 'id' => 52, 250 | 'tree' => 45, 251 | 'lft' => 12, 252 | 'rgt' => 13, 253 | 'depth' => 3, 254 | 'name' => 'Node 1.2.2', 255 | ), 256 | 28 => 257 | array ( 258 | 'id' => 55, 259 | 'tree' => 45, 260 | 'lft' => 18, 261 | 'rgt' => 19, 262 | 'depth' => 3, 263 | 'name' => 'Node 2.1.1', 264 | ), 265 | 29 => 266 | array ( 267 | 'id' => 56, 268 | 'tree' => 45, 269 | 'lft' => 20, 270 | 'rgt' => 21, 271 | 'depth' => 3, 272 | 'name' => 'Node 2.1.2', 273 | ), 274 | 30 => 275 | array ( 276 | 'id' => 58, 277 | 'tree' => 45, 278 | 'lft' => 24, 279 | 'rgt' => 25, 280 | 'depth' => 3, 281 | 'name' => 'Node 2.2.1', 282 | ), 283 | 31 => 284 | array ( 285 | 'id' => 59, 286 | 'tree' => 45, 287 | 'lft' => 26, 288 | 'rgt' => 27, 289 | 'depth' => 3, 290 | 'name' => 'Node 2.2.2', 291 | ), 292 | 32 => 293 | array ( 294 | 'id' => 62, 295 | 'tree' => 45, 296 | 'lft' => 32, 297 | 'rgt' => 33, 298 | 'depth' => 3, 299 | 'name' => 'Node 3.1.1', 300 | ), 301 | 33 => 302 | array ( 303 | 'id' => 63, 304 | 'tree' => 45, 305 | 'lft' => 34, 306 | 'rgt' => 35, 307 | 'depth' => 3, 308 | 'name' => 'Node 3.1.2', 309 | ), 310 | 34 => 311 | array ( 312 | 'id' => 65, 313 | 'tree' => 45, 314 | 'lft' => 38, 315 | 'rgt' => 39, 316 | 'depth' => 3, 317 | 'name' => 'Node 3.2.1', 318 | ), 319 | 35 => 320 | array ( 321 | 'id' => 66, 322 | 'tree' => 45, 323 | 'lft' => 40, 324 | 'rgt' => 41, 325 | 'depth' => 3, 326 | 'name' => 'Node 3.2.2', 327 | ), 328 | ); -------------------------------------------------------------------------------- /tests/data/test-leaves-multiple-tree.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 33, 7 | 'tree' => 23, 8 | 'lft' => 18, 9 | 'rgt' => 19, 10 | 'depth' => 3, 11 | 'name' => 'Node 2.1.1', 12 | ), 13 | 1 => 14 | array ( 15 | 'id' => 34, 16 | 'tree' => 23, 17 | 'lft' => 20, 18 | 'rgt' => 21, 19 | 'depth' => 3, 20 | 'name' => 'Node 2.1.2', 21 | ), 22 | 2 => 23 | array ( 24 | 'id' => 36, 25 | 'tree' => 23, 26 | 'lft' => 24, 27 | 'rgt' => 25, 28 | 'depth' => 3, 29 | 'name' => 'Node 2.2.1', 30 | ), 31 | 3 => 32 | array ( 33 | 'id' => 37, 34 | 'tree' => 23, 35 | 'lft' => 26, 36 | 'rgt' => 27, 37 | 'depth' => 3, 38 | 'name' => 'Node 2.2.2', 39 | ), 40 | ); -------------------------------------------------------------------------------- /tests/data/test-leaves-query.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 4, 7 | 'lft' => 4, 8 | 'rgt' => 5, 9 | 'depth' => 3, 10 | 'name' => 'Node 1.1.1', 11 | ), 12 | 1 => 13 | array ( 14 | 'id' => 5, 15 | 'lft' => 6, 16 | 'rgt' => 7, 17 | 'depth' => 3, 18 | 'name' => 'Node 1.1.2', 19 | ), 20 | 2 => 21 | array ( 22 | 'id' => 7, 23 | 'lft' => 10, 24 | 'rgt' => 11, 25 | 'depth' => 3, 26 | 'name' => 'Node 1.2.1', 27 | ), 28 | 3 => 29 | array ( 30 | 'id' => 8, 31 | 'lft' => 12, 32 | 'rgt' => 13, 33 | 'depth' => 3, 34 | 'name' => 'Node 1.2.2', 35 | ), 36 | 4 => 37 | array ( 38 | 'id' => 11, 39 | 'lft' => 18, 40 | 'rgt' => 19, 41 | 'depth' => 3, 42 | 'name' => 'Node 2.1.1', 43 | ), 44 | 5 => 45 | array ( 46 | 'id' => 12, 47 | 'lft' => 20, 48 | 'rgt' => 21, 49 | 'depth' => 3, 50 | 'name' => 'Node 2.1.2', 51 | ), 52 | 6 => 53 | array ( 54 | 'id' => 14, 55 | 'lft' => 24, 56 | 'rgt' => 25, 57 | 'depth' => 3, 58 | 'name' => 'Node 2.2.1', 59 | ), 60 | 7 => 61 | array ( 62 | 'id' => 15, 63 | 'lft' => 26, 64 | 'rgt' => 27, 65 | 'depth' => 3, 66 | 'name' => 'Node 2.2.2', 67 | ), 68 | 8 => 69 | array ( 70 | 'id' => 18, 71 | 'lft' => 32, 72 | 'rgt' => 33, 73 | 'depth' => 3, 74 | 'name' => 'Node 3.1.1', 75 | ), 76 | 9 => 77 | array ( 78 | 'id' => 19, 79 | 'lft' => 34, 80 | 'rgt' => 35, 81 | 'depth' => 3, 82 | 'name' => 'Node 3.1.2', 83 | ), 84 | 10 => 85 | array ( 86 | 'id' => 21, 87 | 'lft' => 38, 88 | 'rgt' => 39, 89 | 'depth' => 3, 90 | 'name' => 'Node 3.2.1', 91 | ), 92 | 11 => 93 | array ( 94 | 'id' => 22, 95 | 'lft' => 40, 96 | 'rgt' => 41, 97 | 'depth' => 3, 98 | 'name' => 'Node 3.2.2', 99 | ), 100 | ); -------------------------------------------------------------------------------- /tests/data/test-leaves.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 11, 7 | 'lft' => 18, 8 | 'rgt' => 19, 9 | 'depth' => 3, 10 | 'name' => 'Node 2.1.1', 11 | ), 12 | 1 => 13 | array ( 14 | 'id' => 12, 15 | 'lft' => 20, 16 | 'rgt' => 21, 17 | 'depth' => 3, 18 | 'name' => 'Node 2.1.2', 19 | ), 20 | 2 => 21 | array ( 22 | 'id' => 14, 23 | 'lft' => 24, 24 | 'rgt' => 25, 25 | 'depth' => 3, 26 | 'name' => 'Node 2.2.1', 27 | ), 28 | 3 => 29 | array ( 30 | 'id' => 15, 31 | 'lft' => 26, 32 | 'rgt' => 27, 33 | 'depth' => 3, 34 | 'name' => 'Node 2.2.2', 35 | ), 36 | ); -------------------------------------------------------------------------------- /tests/data/test-make-root-exists.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/data/test-make-root-new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/data/test-next-multiple-tree.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 38, 7 | 'tree' => 23, 8 | 'lft' => 30, 9 | 'rgt' => 43, 10 | 'depth' => 1, 11 | 'name' => 'Node 3', 12 | ), 13 | ); -------------------------------------------------------------------------------- /tests/data/test-next.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 16, 7 | 'lft' => 30, 8 | 'rgt' => 43, 9 | 'depth' => 1, 10 | 'name' => 'Node 3', 11 | ), 12 | ); -------------------------------------------------------------------------------- /tests/data/test-parents-multiple-tree-with-depth.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 32, 7 | 'tree' => 23, 8 | 'lft' => 17, 9 | 'rgt' => 22, 10 | 'depth' => 2, 11 | 'name' => 'Node 2.1', 12 | ), 13 | ); -------------------------------------------------------------------------------- /tests/data/test-parents-multiple-tree.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 23, 7 | 'tree' => 23, 8 | 'lft' => 1, 9 | 'rgt' => 44, 10 | 'depth' => 0, 11 | 'name' => 'Root 2', 12 | ), 13 | 1 => 14 | array ( 15 | 'id' => 31, 16 | 'tree' => 23, 17 | 'lft' => 16, 18 | 'rgt' => 29, 19 | 'depth' => 1, 20 | 'name' => 'Node 2', 21 | ), 22 | 2 => 23 | array ( 24 | 'id' => 32, 25 | 'tree' => 23, 26 | 'lft' => 17, 27 | 'rgt' => 22, 28 | 'depth' => 2, 29 | 'name' => 'Node 2.1', 30 | ), 31 | ); -------------------------------------------------------------------------------- /tests/data/test-parents-with-depth.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 10, 7 | 'lft' => 17, 8 | 'rgt' => 22, 9 | 'depth' => 2, 10 | 'name' => 'Node 2.1', 11 | ), 12 | ); -------------------------------------------------------------------------------- /tests/data/test-parents.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 1, 7 | 'lft' => 1, 8 | 'rgt' => 44, 9 | 'depth' => 0, 10 | 'name' => 'Root', 11 | ), 12 | 1 => 13 | array ( 14 | 'id' => 9, 15 | 'lft' => 16, 16 | 'rgt' => 29, 17 | 'depth' => 1, 18 | 'name' => 'Node 2', 19 | ), 20 | 2 => 21 | array ( 22 | 'id' => 10, 23 | 'lft' => 17, 24 | 'rgt' => 22, 25 | 'depth' => 2, 26 | 'name' => 'Node 2.1', 27 | ), 28 | ); -------------------------------------------------------------------------------- /tests/data/test-prepend-to-exists-another-tree.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/data/test-prepend-to-exists-down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/data/test-prepend-to-exists-up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/data/test-prepend-to-new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /tests/data/test-prev-multiple-tree.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 24, 7 | 'tree' => 23, 8 | 'lft' => 2, 9 | 'rgt' => 15, 10 | 'depth' => 1, 11 | 'name' => 'Node 1', 12 | ), 13 | ); -------------------------------------------------------------------------------- /tests/data/test-prev.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 2, 7 | 'lft' => 2, 8 | 'rgt' => 15, 9 | 'depth' => 1, 10 | 'name' => 'Node 1', 11 | ), 12 | ); -------------------------------------------------------------------------------- /tests/data/test-roots-multiple-tree-query.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 1, 7 | 'tree' => 1, 8 | 'lft' => 1, 9 | 'rgt' => 44, 10 | 'depth' => 0, 11 | 'name' => 'Root 1', 12 | ), 13 | 1 => 14 | array ( 15 | 'id' => 23, 16 | 'tree' => 23, 17 | 'lft' => 1, 18 | 'rgt' => 44, 19 | 'depth' => 0, 20 | 'name' => 'Root 2', 21 | ), 22 | 2 => 23 | array ( 24 | 'id' => 45, 25 | 'tree' => 45, 26 | 'lft' => 1, 27 | 'rgt' => 44, 28 | 'depth' => 0, 29 | 'name' => 'Root 3', 30 | ), 31 | ); -------------------------------------------------------------------------------- /tests/data/test-roots-query.php: -------------------------------------------------------------------------------- 1 | 5 | array ( 6 | 'id' => 1, 7 | 'lft' => 1, 8 | 'rgt' => 44, 9 | 'depth' => 0, 10 | 'name' => 'Root', 11 | ), 12 | ); -------------------------------------------------------------------------------- /tests/data/test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/migrations/mysql.sql: -------------------------------------------------------------------------------- 1 | /** 2 | * MySQL 3 | */ 4 | 5 | DROP TABLE IF EXISTS `tree`; 6 | 7 | CREATE TABLE `tree` ( 8 | `id` INT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, 9 | `lft` INT(11) NOT NULL, 10 | `rgt` INT(11) NOT NULL, 11 | `depth` INT(11) NOT NULL, 12 | `name` VARCHAR(255) NOT NULL 13 | ); 14 | 15 | DROP TABLE IF EXISTS `multiple_tree`; 16 | 17 | CREATE TABLE `multiple_tree` ( 18 | `id` INT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, 19 | `tree` INT(11), 20 | `lft` INT(11) NOT NULL, 21 | `rgt` INT(11) NOT NULL, 22 | `depth` INT(11) NOT NULL, 23 | `name` VARCHAR(255) NOT NULL 24 | ); 25 | -------------------------------------------------------------------------------- /tests/migrations/sqlite.sql: -------------------------------------------------------------------------------- 1 | /** 2 | * SQLite 3 | */ 4 | 5 | DROP TABLE IF EXISTS "tree"; 6 | 7 | CREATE TABLE "tree" ( 8 | "id" INTEGER NOT NULL PRIMARY KEY, 9 | "lft" INTEGER NOT NULL, 10 | "rgt" INTEGER NOT NULL, 11 | "depth" INTEGER NOT NULL, 12 | "name" TEXT NOT NULL 13 | ); 14 | 15 | DROP TABLE IF EXISTS "multiple_tree"; 16 | 17 | CREATE TABLE "multiple_tree" ( 18 | "id" INTEGER NOT NULL PRIMARY KEY, 19 | "tree" INTEGER, 20 | "lft" INTEGER NOT NULL, 21 | "rgt" INTEGER NOT NULL, 22 | "depth" INTEGER NOT NULL, 23 | "name" TEXT NOT NULL 24 | ); 25 | -------------------------------------------------------------------------------- /tests/models/MultipleTree.php: -------------------------------------------------------------------------------- 1 | NestedSetsBehavior::className(), 40 | 'treeAttribute' => 'tree', 41 | ], 42 | ]; 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function rules() 49 | { 50 | return [ 51 | ['name', 'required'], 52 | ]; 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function transactions() 59 | { 60 | return [ 61 | self::SCENARIO_DEFAULT => self::OP_ALL, 62 | ]; 63 | } 64 | 65 | /** 66 | * @inheritdoc 67 | */ 68 | public static function find() 69 | { 70 | return new MultipleTreeQuery(get_called_class()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/models/MultipleTreeQuery.php: -------------------------------------------------------------------------------- 1 | self::OP_ALL, 58 | ]; 59 | } 60 | 61 | /** 62 | * @inheritdoc 63 | */ 64 | public static function find() 65 | { 66 | return new TreeQuery(get_called_class()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/models/TreeQuery.php: -------------------------------------------------------------------------------- 1 |