├── tests ├── rununit.bat ├── phpunit.xml ├── bootstrap.php ├── models │ ├── NestedSet.php │ └── NestedSetWithManyRoots.php ├── config │ └── test.php ├── schema │ └── test.sql ├── fixtures │ ├── NestedSet.php │ └── NestedSetWithManyRoots.php └── unit │ └── NestedSetBehaviorTest.php ├── .gitignore ├── schema ├── schema.sql └── schema_with_many_roots.sql ├── composer.json ├── upgrade.md ├── LICENSE.md ├── changelog.md ├── readme.md ├── readme_ru.md └── NestedSetBehavior.php /tests/rununit.bat: -------------------------------------------------------------------------------- 1 | phpunit --verbose unit -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | array( 13 | 'class'=>'ext.NestedSetBehavior', 14 | 'hasManyRoots'=>false, 15 | ), 16 | ); 17 | } 18 | 19 | public function rules() 20 | { 21 | return array( 22 | array('name','required'), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/models/NestedSetWithManyRoots.php: -------------------------------------------------------------------------------- 1 | array( 13 | 'class'=>'ext.NestedSetBehavior', 14 | 'hasManyRoots'=>true, 15 | ), 16 | ); 17 | } 18 | 19 | public function rules() 20 | { 21 | return array( 22 | array('name','required'), 23 | ); 24 | } 25 | } -------------------------------------------------------------------------------- /tests/config/test.php: -------------------------------------------------------------------------------- 1 | dirname(__FILE__).'/..', 4 | 'extensionPath'=>dirname(__FILE__).'/../..', 5 | 6 | 'import'=>array( 7 | 'application.models.*', 8 | ), 9 | 10 | 'components'=>array( 11 | 'fixture'=>array( 12 | 'class'=>'system.test.CDbFixtureManager', 13 | 'basePath'=>dirname(__FILE__).'/../fixtures', 14 | ), 15 | 'db'=>array( 16 | 'connectionString'=>'mysql:host=localhost;dbname=test', 17 | 'username'=>'root', 18 | 'password'=>'', 19 | 'charset'=>'utf8', 20 | ), 21 | ), 22 | ); 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiiext/nested-set-behavior", 3 | "description": "AR models behavior that allows to work with nested sets tree.", 4 | "keywords": ["yii", "extension", "nested set"], 5 | "homepage": "https://github.com/yiiext/nested-set-behavior", 6 | "type": "yii-extension", 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Alexander Kochetov", 11 | "email": "creocoder@gmail.com" 12 | } 13 | ], 14 | "minimum-stability": "dev", 15 | "autoload" : { 16 | "classmap" : ["NestedSetBehavior.php"] 17 | }, 18 | "require": { 19 | "php": ">=5.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /upgrade.md: -------------------------------------------------------------------------------- 1 | Upgrading instructions for NestedSetBehavior v1.0.6 2 | =================================================== 3 | 4 | !!!IMPORTANT!!! 5 | 6 | The following upgrading instructions are cumulative. That is, 7 | if you want to upgrade from version A to version C and there is 8 | version B between A and C, you need to following the instructions 9 | for both A and B. 10 | 11 | Upgrading from v1.0.5 12 | --------------------- 13 | 14 | - You need to change following code: 15 | 16 | ~~~ 17 | $parent=$node->getParent(); 18 | $prevSibling=$node->getPrevSibling(); 19 | $nextSibling=$node->getNextSibling(); 20 | ~~~ 21 | 22 | to 23 | 24 | ~~~ 25 | $parent=$node->parent()->find(); 26 | $prevSibling=$node->prev()->find(); 27 | $nextSibling=$node->next()->find(); 28 | ~~~ 29 | -------------------------------------------------------------------------------- /tests/schema/test.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `NestedSet`; 2 | 3 | CREATE TABLE `NestedSet` ( 4 | `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, 5 | `lft` INT(10) UNSIGNED NOT NULL, 6 | `rgt` INT(10) UNSIGNED NOT NULL, 7 | `level` SMALLINT(5) UNSIGNED NOT NULL, 8 | `name` VARCHAR(50) NOT NULL, 9 | PRIMARY KEY (`id`), 10 | KEY `lft` (`lft`), 11 | KEY `rgt` (`rgt`), 12 | KEY `level` (`level`) 13 | ); 14 | 15 | DROP TABLE IF EXISTS `NestedSetWithManyRoots`; 16 | 17 | CREATE TABLE `NestedSetWithManyRoots` ( 18 | `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, 19 | `root` INT(10) UNSIGNED DEFAULT NULL, 20 | `lft` INT(10) UNSIGNED NOT NULL, 21 | `rgt` INT(10) UNSIGNED NOT NULL, 22 | `level` SMALLINT(5) UNSIGNED NOT NULL, 23 | `name` VARCHAR(50) NOT NULL, 24 | PRIMARY KEY (`id`), 25 | KEY `root` (`root`), 26 | KEY `lft` (`lft`), 27 | KEY `rgt` (`rgt`), 28 | KEY `level` (`level`) 29 | ); -------------------------------------------------------------------------------- /tests/fixtures/NestedSet.php: -------------------------------------------------------------------------------- 1 | array( 4 | 'id'=>1, 5 | 'root'=>1, 6 | 'lft'=>1, 7 | 'rgt'=>14, 8 | 'level'=>1, 9 | 'name'=>'category1', 10 | ), 11 | 'category1_1'=>array( 12 | 'id'=>2, 13 | 'root'=>1, 14 | 'lft'=>2, 15 | 'rgt'=>7, 16 | 'level'=>2, 17 | 'name'=>'category1_1', 18 | ), 19 | 'category1_1_1'=>array( 20 | 'id'=>3, 21 | 'root'=>1, 22 | 'lft'=>3, 23 | 'rgt'=>4, 24 | 'level'=>3, 25 | 'name'=>'category1_1_1', 26 | ), 27 | 'category1_1_2'=>array( 28 | 'id'=>4, 29 | 'root'=>1, 30 | 'lft'=>5, 31 | 'rgt'=>6, 32 | 'level'=>3, 33 | 'name'=>'category1_1_2', 34 | ), 35 | 'category1_2'=>array( 36 | 'id'=>5, 37 | 'root'=>1, 38 | 'lft'=>8, 39 | 'rgt'=>13, 40 | 'level'=>2, 41 | 'name'=>'category1_2', 42 | ), 43 | 'category1_2_1'=>array( 44 | 'id'=>6, 45 | 'root'=>1, 46 | 'lft'=>9, 47 | 'rgt'=>10, 48 | 'level'=>3, 49 | 'name'=>'category1_2_1', 50 | ), 51 | 'category1_2_2'=>array( 52 | 'id'=>7, 53 | 'root'=>1, 54 | 'lft'=>11, 55 | 'rgt'=>12, 56 | 'level'=>3, 57 | 'name'=>'category1_2_2', 58 | ), 59 | ); -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2010 by yiiext. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 1.06 2 | ---- 3 | 4 | - Changed the name of the methods getParent(), getPrevSibling(), getNextSibling() to parent(), prev(), next(). Also changed the return type of methods. (creocoder) 5 | - Updated readmes. (creocoder) 6 | - Added upgrade. (creocoder) 7 | 8 | 1.05 9 | ---- 10 | 11 | - External transaction support for NestedSetBehavior::delete() method. (creocoder) 12 | 13 | 1.04 14 | ---- 15 | 16 | - Fix for $depth parameter of NestedSetBehavior::ancestors() method. (creocoder) 17 | 18 | 1.03 19 | ---- 20 | 21 | - Class renamed to NestedSetBehavior. (creocoder) 22 | - Rare bug with cache correction after NestedSetBehavior::addNode() fixed. (creocoder) 23 | - Readmes non-recursive tree traversal algorithm fix. (creocoder) 24 | 25 | 1.02 26 | ---- 27 | 28 | - Added moveAsRoot() method. (creocoder) 29 | - Updated readmes. (Sam Dark) 30 | 31 | 1.01 32 | ---- 33 | 34 | - Added not integer pk support. (creocoder) 35 | - Remove `final` from class. (creocoder) 36 | 37 | 1.00 38 | ---- 39 | 40 | - Some behavior refactoring before release. (creocoder) 41 | - Advanced doc added. (creocoder) 42 | 43 | 0.99b 44 | ---- 45 | 46 | - Allow to use multiply change tree operations. (creocoder) 47 | - Method saveNode() now create root in "single root mode". (creocoder) 48 | - Unit tests refactored. (creocoder) 49 | - Some behavior refactoring. (creocoder) 50 | 51 | 0.99 52 | ---- 53 | 54 | - Added note about removing fields from `rules`. (Sam Dark) 55 | - Added Unit tests for single root mode. (creocoder) 56 | - All attributes are now correctly quoted. (creocoder) 57 | - Renamed fields: 'root'=>'rootAttribute', 'left'=>'leftAttribute', 'right'=>'rightAttribute', 'level'=>'levelAttribute'. (creocoder) 58 | - Renamed method parent() => getParent(). (creocoder) 59 | 60 | 0.95 61 | ---- 62 | 63 | - Unit tests added. (creocoder) 64 | - Incorrect usage of save() and delete() instead of behavior's saveNode() and deleteNode() is now detected. (creocoder) 65 | 66 | 0.90 67 | ---- 68 | 69 | - Moving a node to a different root is now supported. (creocoder) 70 | 71 | 0.85 72 | ---- 73 | 74 | - Initial public release. (creocoder) -------------------------------------------------------------------------------- /tests/fixtures/NestedSetWithManyRoots.php: -------------------------------------------------------------------------------- 1 | array( 4 | 'id'=>1, 5 | 'root'=>1, 6 | 'lft'=>1, 7 | 'rgt'=>14, 8 | 'level'=>1, 9 | 'name'=>'category1', 10 | ), 11 | 'category1_1'=>array( 12 | 'id'=>2, 13 | 'root'=>1, 14 | 'lft'=>2, 15 | 'rgt'=>7, 16 | 'level'=>2, 17 | 'name'=>'category1_1', 18 | ), 19 | 'category1_1_1'=>array( 20 | 'id'=>3, 21 | 'root'=>1, 22 | 'lft'=>3, 23 | 'rgt'=>4, 24 | 'level'=>3, 25 | 'name'=>'category1_1_1', 26 | ), 27 | 'category1_1_2'=>array( 28 | 'id'=>4, 29 | 'root'=>1, 30 | 'lft'=>5, 31 | 'rgt'=>6, 32 | 'level'=>3, 33 | 'name'=>'category1_1_2', 34 | ), 35 | 'category1_2'=>array( 36 | 'id'=>5, 37 | 'root'=>1, 38 | 'lft'=>8, 39 | 'rgt'=>13, 40 | 'level'=>2, 41 | 'name'=>'category1_2', 42 | ), 43 | 'category1_2_1'=>array( 44 | 'id'=>6, 45 | 'root'=>1, 46 | 'lft'=>9, 47 | 'rgt'=>10, 48 | 'level'=>3, 49 | 'name'=>'category1_2_1', 50 | ), 51 | 'category1_2_2'=>array( 52 | 'id'=>7, 53 | 'root'=>1, 54 | 'lft'=>11, 55 | 'rgt'=>12, 56 | 'level'=>3, 57 | 'name'=>'category1_2_2', 58 | ), 59 | 'category2'=>array( 60 | 'id'=>8, 61 | 'root'=>8, 62 | 'lft'=>1, 63 | 'rgt'=>14, 64 | 'level'=>1, 65 | 'name'=>'category2', 66 | ), 67 | 'category2_1'=>array( 68 | 'id'=>9, 69 | 'root'=>8, 70 | 'lft'=>2, 71 | 'rgt'=>7, 72 | 'level'=>2, 73 | 'name'=>'category2_1', 74 | ), 75 | 'category2_1_1'=>array( 76 | 'id'=>10, 77 | 'root'=>8, 78 | 'lft'=>3, 79 | 'rgt'=>4, 80 | 'level'=>3, 81 | 'name'=>'category2_1_1', 82 | ), 83 | 'category2_1_2'=>array( 84 | 'id'=>11, 85 | 'root'=>8, 86 | 'lft'=>5, 87 | 'rgt'=>6, 88 | 'level'=>3, 89 | 'name'=>'category2_1_2', 90 | ), 91 | 'category2_2'=>array( 92 | 'id'=>12, 93 | 'root'=>8, 94 | 'lft'=>8, 95 | 'rgt'=>13, 96 | 'level'=>2, 97 | 'name'=>'category2_2', 98 | ), 99 | 'category2_2_1'=>array( 100 | 'id'=>13, 101 | 'root'=>8, 102 | 'lft'=>9, 103 | 'rgt'=>10, 104 | 'level'=>3, 105 | 'name'=>'category2_2_1', 106 | ), 107 | 'category2_2_2'=>array( 108 | 'id'=>14, 109 | 'root'=>8, 110 | 'lft'=>11, 111 | 'rgt'=>12, 112 | 'level'=>3, 113 | 'name'=>'category2_2_2', 114 | ), 115 | ); -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Nested Set 2 | ========== 3 | 4 | Nested Set behavior for Yii 2: https://github.com/creocoder/yii2-nested-sets 5 | 6 | This extension allows managing trees stored in database as nested sets. 7 | It's implemented as Active Record behavior. 8 | 9 | Installing and configuring 10 | -------------------------- 11 | 12 | First you need to configure model as follows: 13 | 14 | ```php 15 | public function behaviors() 16 | { 17 | return array( 18 | 'nestedSetBehavior'=>array( 19 | 'class'=>'ext.yiiext.behaviors.model.trees.NestedSetBehavior', 20 | 'leftAttribute'=>'lft', 21 | 'rightAttribute'=>'rgt', 22 | 'levelAttribute'=>'level', 23 | ), 24 | ); 25 | } 26 | ``` 27 | 28 | There is no need to validate fields specified in `leftAttribute`, 29 | `rightAttribute`, `rootAttribute` and `levelAttribute` options. Moreover, 30 | there could be problems if there are validation rules for these. Please 31 | check if there are no rules for fields mentioned in model's rules() method. 32 | 33 | In case of storing a single tree per database, DB structure can be built with 34 | `extensions/yiiext/behaviors/trees/schema.sql`. If you're going to store multiple 35 | trees you'll need `extensions/yiiext/behaviors/trees/schema_many_roots.sql`. 36 | 37 | By default `leftAttribute`, `rightAttribute` and `levelAttribute` values are 38 | matching field names in default DB schemas so you can skip configuring these. 39 | 40 | There are two ways this behavior can work: one tree per table and multiple trees 41 | per table. The mode is selected based on the value of `hasManyRoots` option that 42 | is `false` by default meaning single tree mode. In multiple trees mode you can 43 | set `rootAttribute` option to match existing field in the table storing the tree. 44 | 45 | Selecting from a tree 46 | --------------------- 47 | 48 | In the following we'll use an example model `Category` with the following in its 49 | DB: 50 | 51 | ~~~ 52 | - 1. Mobile phones 53 | - 2. iPhone 54 | - 3. Samsung 55 | - 4. X100 56 | - 5. C200 57 | - 6. Motorola 58 | - 7. Cars 59 | - 8. Audi 60 | - 9. Ford 61 | - 10. Mercedes 62 | ~~~ 63 | 64 | In this example we have two trees. Tree roots are ones with ID=1 and ID=7. 65 | 66 | ### Getting all roots 67 | 68 | Using `NestedSetBehavior::roots()`: 69 | 70 | ```php 71 | $roots=Category::model()->roots()->findAll(); 72 | ``` 73 | 74 | Result: 75 | 76 | Array of Active Record objects corresponding to Mobile phones and Cars nodes. 77 | 78 | ### Getting all descendants of a node 79 | 80 | Using `NestedSetBehavior::descendants()`: 81 | 82 | ```php 83 | $category=Category::model()->findByPk(1); 84 | $descendants=$category->descendants()->findAll(); 85 | ``` 86 | 87 | Result: 88 | 89 | Array of Active Record objects corresponding to iPhone, Samsung, X100, C200 and Motorola. 90 | 91 | ### Getting all children of a node 92 | 93 | Using `NestedSetBehavior::children()`: 94 | 95 | ```php 96 | $category=Category::model()->findByPk(1); 97 | $descendants=$category->children()->findAll(); 98 | ``` 99 | 100 | Result: 101 | 102 | Array of Active Record objects corresponding to iPhone, Samsung and Motorola. 103 | 104 | ### Getting all ancestors of a node 105 | 106 | Using `NestedSetBehavior::ancestors()`: 107 | 108 | ```php 109 | $category=Category::model()->findByPk(5); 110 | $ancestors=$category->ancestors()->findAll(); 111 | ``` 112 | 113 | Result: 114 | 115 | Array of Active Record objects corresponding to Samsung and Mobile phones. 116 | 117 | ### Getting parent of a node 118 | 119 | Using `NestedSetBehavior::parent()`: 120 | 121 | ```php 122 | $category=Category::model()->findByPk(9); 123 | $parent=$category->parent()->find(); 124 | ``` 125 | 126 | Result: 127 | 128 | Array of Active Record objects corresponding to Cars. 129 | 130 | ### Getting node siblings 131 | 132 | Using `NestedSetBehavior::prev()` or 133 | `NestedSetBehavior::next()`: 134 | 135 | ```php 136 | $category=Category::model()->findByPk(9); 137 | $nextSibling=$category->next()->find(); 138 | ``` 139 | 140 | Result: 141 | 142 | Array of Active Record objects corresponding to Mercedes. 143 | 144 | ### Getting the whole tree 145 | 146 | You can get the whole tree using standard AR methods like the following. 147 | 148 | For single tree per table: 149 | 150 | ```php 151 | Category::model()->findAll(array('order'=>'lft')); 152 | ``` 153 | 154 | For multiple trees per table: 155 | 156 | ```php 157 | Category::model()->findAll(array('condition'=>'root=?','order'=>'lft'),array($root_id)); 158 | ``` 159 | 160 | Modifying a tree 161 | ---------------- 162 | 163 | In this section we'll build a tree like the one used in the previous section. 164 | 165 | ### Creating root nodes 166 | 167 | You can create a root node using `NestedSetBehavior::saveNode()`. 168 | In a single tree per table mode you can create only one root node. If you'll attempt 169 | to create more there will be CException thrown. 170 | 171 | ```php 172 | $root=new Category; 173 | $root->title='Mobile Phones'; 174 | $root->saveNode(); 175 | $root=new Category; 176 | $root->title='Cars'; 177 | $root->saveNode(); 178 | ``` 179 | 180 | Result: 181 | 182 | ~~~ 183 | - 1. Mobile Phones 184 | - 2. Cars 185 | ~~~ 186 | 187 | ### Adding child nodes 188 | 189 | There are multiple methods allowing you adding child nodes. To get more info 190 | about these refer to API. Let's use these 191 | to add nodes to the tree we have: 192 | 193 | ```php 194 | $category1=new Category; 195 | $category1->title='Ford'; 196 | $category2=new Category; 197 | $category2->title='Mercedes'; 198 | $category3=new Category; 199 | $category3->title='Audi'; 200 | $root=Category::model()->findByPk(1); 201 | $category1->appendTo($root); 202 | $category2->insertAfter($category1); 203 | $category3->insertBefore($category1); 204 | ``` 205 | 206 | Result: 207 | 208 | ~~~ 209 | - 1. Mobile phones 210 | - 3. Audi 211 | - 4. Ford 212 | - 5. Mercedes 213 | - 2. Cars 214 | ~~~ 215 | 216 | Logically the tree above doesn't looks correct. We'll fix it later. 217 | 218 | ```php 219 | $category1=new Category; 220 | $category1->title='Samsung'; 221 | $category2=new Category; 222 | $category2->title='Motorola'; 223 | $category3=new Category; 224 | $category3->title='iPhone'; 225 | $root=Category::model()->findByPk(2); 226 | $category1->appendTo($root); 227 | $category2->insertAfter($category1); 228 | $category3->prependTo($root); 229 | ``` 230 | 231 | Result: 232 | 233 | ~~~ 234 | - 1. Mobile phones 235 | - 3. Audi 236 | - 4. Ford 237 | - 5. Mercedes 238 | - 2. Cars 239 | - 6. iPhone 240 | - 7. Samsung 241 | - 8. Motorola 242 | ~~~ 243 | 244 | ```php 245 | $category1=new Category; 246 | $category1->title='X100'; 247 | $category2=new Category; 248 | $category2->title='C200'; 249 | $node=Category::model()->findByPk(3); 250 | $category1->appendTo($node); 251 | $category2->prependTo($node); 252 | ``` 253 | 254 | Result: 255 | 256 | ~~~ 257 | - 1. Mobile phones 258 | - 3. Audi 259 | - 9. С200 260 | - 10. X100 261 | - 4. Ford 262 | - 5. Mercedes 263 | - 2. Cars 264 | - 6. iPhone 265 | - 7. Samsung 266 | - 8. Motorola 267 | ~~~ 268 | 269 | Modifying a tree 270 | ---------------- 271 | 272 | In this section we'll finally make our tree logical. 273 | 274 | ### Tree modification methods 275 | 276 | There are several methods allowing you to modify a tree. To get more info 277 | about these refer to API. 278 | 279 | Let's start: 280 | 281 | ```php 282 | // move phones to the proper place 283 | $x100=Category::model()->findByPk(10); 284 | $c200=Category::model()->findByPk(9); 285 | $samsung=Category::model()->findByPk(7); 286 | $x100->moveAsFirst($samsung); 287 | $c200->moveBefore($x100); 288 | // now move all Samsung phones branch 289 | $mobile_phones=Category::model()->findByPk(1); 290 | $samsung->moveAsFirst($mobile_phones); 291 | // move the rest of phone models 292 | $iphone=Category::model()->findByPk(6); 293 | $iphone->moveAsFirst($mobile_phones); 294 | $motorola=Category::model()->findByPk(8); 295 | $motorola->moveAfter($samsung); 296 | // move car models to appropriate place 297 | $cars=Category::model()->findByPk(2); 298 | $audi=Category::model()->findByPk(3); 299 | $ford=Category::model()->findByPk(4); 300 | $mercedes=Category::model()->findByPk(5); 301 | 302 | foreach(array($audi,$ford,$mercedes) as $category) 303 | $category->moveAsLast($cars); 304 | ``` 305 | 306 | Result: 307 | 308 | ~~~ 309 | - 1. Mobile phones 310 | - 6. iPhone 311 | - 7. Samsung 312 | - 10. X100 313 | - 9. С200 314 | - 8. Motorola 315 | - 2. Cars 316 | - 3. Audi 317 | - 4. Ford 318 | - 5. Mercedes 319 | ~~~ 320 | 321 | ### Moving a node making it a new root 322 | 323 | There is a special `moveAsRoot()` method that allows moving a node and making it 324 | a new root. All descendants are moved as well in this case. 325 | 326 | Example: 327 | 328 | ```php 329 | $node=Category::model()->findByPk(10); 330 | $node->moveAsRoot(); 331 | ``` 332 | 333 | ### Identifying node type 334 | 335 | There are three methods to get node type: `isRoot()`, `isLeaf()`, `isDescendantOf()`. 336 | 337 | Example: 338 | 339 | ```php 340 | $root=Category::model()->findByPk(1); 341 | CVarDumper::dump($root->isRoot()); //true; 342 | CVarDumper::dump($root->isLeaf()); //false; 343 | $node=Category::model()->findByPk(9); 344 | CVarDumper::dump($node->isDescendantOf($root)); //true; 345 | CVarDumper::dump($node->isRoot()); //false; 346 | CVarDumper::dump($node->isLeaf()); //true; 347 | $samsung=Category::model()->findByPk(7); 348 | CVarDumper::dump($node->isDescendantOf($samsung)); //true; 349 | ``` 350 | 351 | Useful code 352 | ------------ 353 | 354 | ### Non-recursive tree traversal 355 | 356 | ```php 357 | $criteria=new CDbCriteria; 358 | $criteria->order='t.lft'; // or 't.root, t.lft' for multiple trees 359 | $categories=Category::model()->findAll($criteria); 360 | $level=0; 361 | 362 | foreach($categories as $n=>$category) 363 | { 364 | if($category->level==$level) 365 | echo CHtml::closeTag('li')."\n"; 366 | else if($category->level>$level) 367 | echo CHtml::openTag('ul')."\n"; 368 | else 369 | { 370 | echo CHtml::closeTag('li')."\n"; 371 | 372 | for($i=$level-$category->level;$i;$i--) 373 | { 374 | echo CHtml::closeTag('ul')."\n"; 375 | echo CHtml::closeTag('li')."\n"; 376 | } 377 | } 378 | 379 | echo CHtml::openTag('li'); 380 | echo CHtml::encode($category->title); 381 | $level=$category->level; 382 | } 383 | 384 | for($i=$level;$i;$i--) 385 | { 386 | echo CHtml::closeTag('li')."\n"; 387 | echo CHtml::closeTag('ul')."\n"; 388 | } 389 | ``` 390 | -------------------------------------------------------------------------------- /readme_ru.md: -------------------------------------------------------------------------------- 1 | Nested Set 2 | ========== 3 | 4 | Nested Set behavior для Yii 2: https://github.com/creocoder/yii2-nested-sets 5 | 6 | Этот компонент предназначен для работы с деревьями, которые хранятся в виде 7 | вложенных множеств. Реализация компонента выполнена в виде поведения для 8 | Active Record моделей. 9 | 10 | Установка и настройка 11 | --------------------- 12 | 13 | Для начала работы необходимо сконфигурировать модель следующим образом: 14 | 15 | ~~~php 16 | public function behaviors() 17 | { 18 | return array( 19 | 'nestedSetBehavior'=>array( 20 | 'class'=>'ext.yiiext.behaviors.model.trees.NestedSetBehavior', 21 | 'leftAttribute'=>'lft', 22 | 'rightAttribute'=>'rgt', 23 | 'levelAttribute'=>'level', 24 | ), 25 | ); 26 | } 27 | ~~~ 28 | 29 | Необходимости в валидации полей, которые определяются опциями `leftAttribute`, 30 | `rightAttribute`, `rootAttribute` и `levelAttribute` нет. Кроме того, могут 31 | возникнуть проблемы с некоторыми методами поведения, если для данных полей 32 | имеются правила валидации. Проверьте их отсутствие в методе rules() модели. 33 | 34 | Структура базы данных может быть аналогична 35 | `extensions/yiiext/behaviors/trees/schema.sql` в случае если в таблице планируется 36 | хранение только одного дерева. Если в таблице необходимо хранить множество деревьев, 37 | то подойдет схема `extensions/yiiext/behaviors/trees/schema_many_roots.sql`. 38 | 39 | Значения опций `leftAttribute`, `rightAttribute` и `levelAttribute` по умолчанию 40 | совпадают с названием полей в вышеприведенных схемах, поэтому при конфигурации 41 | поведения их можно опустить. 42 | 43 | У поведения существует два режима работы: одно дерево и много деревьев. 44 | Режим работы управляется опцией `hasManyRoots`, которая по умолчанию имеет 45 | значение `false`. В режиме работы «много деревьев» возможно использование 46 | ещё одной опции `rootAttribute`, значение которой по умолчанию также совпадает 47 | с названием поля в соответствующей схеме. 48 | 49 | Описание работы методов выборки 50 | ------------------------------- 51 | 52 | В дальнейшем все особенности работы методов будут рассмотрены в контексте 53 | конкретного дерева. Допустим у нас есть модель `Category`, а в базе данных 54 | хранится следующая структура: 55 | 56 | ~~~ 57 | - 1. Mobile phones 58 | - 2. iPhone 59 | - 3. Samsung 60 | - 4. X100 61 | - 5. C200 62 | - 6. Motorola 63 | - 7. Cars 64 | - 8. Audi 65 | - 9. Ford 66 | - 10. Mercedes 67 | ~~~ 68 | 69 | В этом примере в таблице хранится два дерева, корнями которых являются 70 | соответственно узлы с ID=1 и ID=7. 71 | 72 | ### Выборка всех корней 73 | 74 | Используем метод `NestedSetBehavior::roots()`: 75 | 76 | ~~~php 77 | $roots=Category::model()->roots()->findAll(); 78 | ~~~ 79 | 80 | Результат: 81 | 82 | Массив объектов Active Record, которые характеризуют узлы Mobile phones и Cars. 83 | 84 | ### Выборка всех потомков узла 85 | 86 | Используем метод `NestedSetBehavior::descendants()`: 87 | 88 | ~~~php 89 | $category=Category::model()->findByPk(1); 90 | $descendants=$category->descendants()->findAll(); 91 | ~~~ 92 | 93 | Результат: 94 | 95 | Массив объектов Active Record, которые характеризуют узлы iPhone, Samsung, X100, C200 и Motorola. 96 | 97 | ### Выборка прямых потомков узла 98 | 99 | Используем метод `NestedSetBehavior::children()`: 100 | 101 | ~~~php 102 | $category=Category::model()->findByPk(1); 103 | $descendants=$category->children()->findAll(); 104 | ~~~ 105 | 106 | Результат: 107 | 108 | Массив объектов Active Record, которые характеризуют узлы iPhone, Samsung и Motorola. 109 | 110 | ### Выборка всех предков узла 111 | 112 | Используем метод `NestedSetBehavior::ancestors()`: 113 | 114 | ~~~php 115 | $category=Category::model()->findByPk(5); 116 | $ancestors=$category->ancestors()->findAll(); 117 | ~~~ 118 | 119 | Результат: 120 | 121 | Массив объектов Active Record, которые характеризуют узлы Samsung и Mobile phones. 122 | 123 | ### Выборка предка узла 124 | 125 | Используем метод `NestedSetBehavior::parent()`: 126 | 127 | ~~~php 128 | $category=Category::model()->findByPk(9); 129 | $parent=$category->parent()->find(); 130 | ~~~ 131 | 132 | Результат: 133 | 134 | Объект Active Record, который характеризует узел Cars. 135 | 136 | ### Выборка соседей узла 137 | 138 | Используем методы `NestedSetBehavior::prev()` или 139 | `NestedSetBehavior::next()`: 140 | 141 | ~~~php 142 | $category=Category::model()->findByPk(9); 143 | $nextSibling=$category->next()->find(); 144 | ~~~ 145 | 146 | Результат: 147 | 148 | Объект Active Record, который характеризует узел Mercedes. 149 | 150 | ### Выборка дерева целиком 151 | 152 | Это может быть осуществлено при помощи стандартных методов Active Record. 153 | 154 | Для режима «одно дерево»: 155 | ~~~php 156 | Category::model()->findAll(array('order'=>'lft')); 157 | ~~~ 158 | 159 | Для режима «много деревьев»: 160 | ~~~php 161 | Category::model()->findAll(array('condition'=>'root=?','order'=>'lft'),array($root)); 162 | ~~~ 163 | 164 | Описание работы методов создания узлов 165 | -------------------------------------- 166 | 167 | В этом разделе мы построим дерево похожее на то, которое было приведено в предыдущем разделе. 168 | 169 | ### Создание корневых узлов 170 | 171 | Создание корня может быть осуществлено при помощи метода NestedSetBehavior::saveNode(). 172 | В режиме работы «одно дерево» может быть создан только один корень, в противном 173 | случае вы получите CException. 174 | 175 | ~~~php 176 | $root=new Category; 177 | $root->title='Mobile Phones'; 178 | $root->saveNode(); 179 | $root=new Category; 180 | $root->title='Cars'; 181 | $root->saveNode(); 182 | ~~~ 183 | 184 | Результат в виде дерева: 185 | 186 | ~~~ 187 | - 1. Mobile Phones 188 | - 2. Cars 189 | ~~~ 190 | 191 | ### Добавление дочерних узлов 192 | 193 | Для добавление дочерних узлов поведение содержит много методов, использование 194 | которых будет показано на примерах. Более подробно об этих методах можно прочитать в API. 195 | 196 | Продолжим работать с деревом, полученным в предыдущем разделе: 197 | ~~~php 198 | $category1=new Category; 199 | $category1->title='Ford'; 200 | $category2=new Category; 201 | $category2->title='Mercedes'; 202 | $category3=new Category; 203 | $category3->title='Audi'; 204 | $root=Category::model()->findByPk(1); 205 | $category1->appendTo($root); 206 | $category2->insertAfter($category1); 207 | $category3->insertBefore($category1); 208 | ~~~ 209 | 210 | Результат в виде дерева: 211 | 212 | ~~~ 213 | - 1. Mobile phones 214 | - 3. Audi 215 | - 4. Ford 216 | - 5. Mercedes 217 | - 2. Cars 218 | ~~~ 219 | 220 | Можно заметить, что это некорректно с точки зрения логики, но в следующих разделах мы это исправим. 221 | 222 | Продолжаем: 223 | ~~~php 224 | $category1=new Category; 225 | $category1->title='Samsung'; 226 | $category2=new Category; 227 | $category2->title='Motorola'; 228 | $category3=new Category; 229 | $category3->title='iPhone'; 230 | $root=Category::model()->findByPk(2); 231 | $category1->appendTo($root); 232 | $category2->insertAfter($category1); 233 | $category3->prependTo($root); 234 | ~~~ 235 | 236 | Результат в виде дерева: 237 | 238 | ~~~ 239 | - 1. Mobile phones 240 | - 3. Audi 241 | - 4. Ford 242 | - 5. Mercedes 243 | - 2. Cars 244 | - 6. iPhone 245 | - 7. Samsung 246 | - 8. Motorola 247 | ~~~ 248 | 249 | И снова, не обращаем внимание на нелогичность дерева. 250 | 251 | Продолжаем: 252 | ~~~php 253 | $category1=new Category; 254 | $category1->title='X100'; 255 | $category2=new Category; 256 | $category2->title='C200'; 257 | $node=Category::model()->findByPk(3); 258 | $category1->appendTo($node); 259 | $category2->prependTo($node); 260 | ~~~ 261 | 262 | Результат в виде дерева: 263 | 264 | ~~~ 265 | - 1. Mobile phones 266 | - 3. Audi 267 | - 9. С200 268 | - 10. X100 269 | - 4. Ford 270 | - 5. Mercedes 271 | - 2. Cars 272 | - 6. iPhone 273 | - 7. Samsung 274 | - 8. Motorola 275 | ~~~ 276 | 277 | Методы модифицирующие дерево 278 | ---------------------------- 279 | 280 | В этом разделе мы окончательно преобразуем дерево к должному виду. 281 | 282 | ### Методы перемещения узлов 283 | 284 | Этих методов также довольно много, поэтому использование будет показано на 285 | примерах, а более подробно обо всех можно узнать в API. 286 | 287 | Начнем модификацию дерева: 288 | ~~~php 289 | // сначала переместим модели телефонов на место 290 | $x100=Category::model()->findByPk(10); 291 | $c200=Category::model()->findByPk(9); 292 | $samsung=Category::model()->findByPk(7); 293 | $x100->moveAsFirst($samsung); 294 | $c200->moveBefore($x100); 295 | // теперь переместим всю ветку с телефонами Samsung 296 | $mobile_phones=Category::model()->findByPk(1); 297 | $samsung->moveAsFirst($mobile_phones); 298 | // переместим остальные модели телефонов 299 | $iphone=Category::model()->findByPk(6); 300 | $iphone->moveAsFirst($mobile_phones); 301 | $motorola=Category::model()->findByPk(8); 302 | $motorola->moveAfter($samsung); 303 | // переместим модели машин на место 304 | $cars=Category::model()->findByPk(2); 305 | $audi=Category::model()->findByPk(3); 306 | $ford=Category::model()->findByPk(4); 307 | $mercedes=Category::model()->findByPk(5); 308 | 309 | foreach(array($audi,$ford,$mercedes) as $category) 310 | $category->moveAsLast($cars); 311 | ~~~ 312 | 313 | Результат в виде дерева: 314 | 315 | ~~~ 316 | - 1. Mobile phones 317 | - 6. iPhone 318 | - 7. Samsung 319 | - 10. X100 320 | - 9. С200 321 | - 8. Motorola 322 | - 2. Cars 323 | - 3. Audi 324 | - 4. Ford 325 | - 5. Mercedes 326 | ~~~ 327 | 328 | ### Перемещение узла в качестве нового корня 329 | 330 | Для этого в поведении присутствует метод `moveAsRoot()`, который преобразует узел 331 | в новый корень, а все его дочерние узлы становятся потомками нового корня. 332 | 333 | Пример использования: 334 | ~~~php 335 | $node=Category::model()->findByPk(10); 336 | $node->moveAsRoot(); 337 | ~~~ 338 | 339 | ### Идентификация узлов дерева 340 | 341 | Для этого в поведении присутствуют методы `isRoot()`, `isLeaf()`, `isDescendantOf()`. 342 | 343 | Пример использования: 344 | ~~~php 345 | $root=Category::model()->findByPk(1); 346 | CVarDumper::dump($root->isRoot()); //true; 347 | CVarDumper::dump($root->isLeaf()); //false; 348 | $node=Category::model()->findByPk(9); 349 | CVarDumper::dump($node->isDescendantOf($root)); //true; 350 | CVarDumper::dump($node->isRoot()); //false; 351 | CVarDumper::dump($node->isLeaf()); //true; 352 | $samsung=Category::model()->findByPk(7); 353 | CVarDumper::dump($node->isDescendantOf($samsung)); //true; 354 | ~~~ 355 | 356 | Полезный код 357 | ------------ 358 | 359 | ### Обход дерева без рекурсии 360 | 361 | ~~~php 362 | $criteria=new CDbCriteria; 363 | $criteria->order='t.lft'; // или 't.root, t.lft' для множественных деревьев 364 | $categories=Category::model()->findAll($criteria); 365 | $level=0; 366 | 367 | foreach($categories as $n=>$category) 368 | { 369 | if($category->level==$level) 370 | echo CHtml::closeTag('li')."\n"; 371 | else if($category->level>$level) 372 | echo CHtml::openTag('ul')."\n"; 373 | else 374 | { 375 | echo CHtml::closeTag('li')."\n"; 376 | 377 | for($i=$level-$category->level;$i;$i--) 378 | { 379 | echo CHtml::closeTag('ul')."\n"; 380 | echo CHtml::closeTag('li')."\n"; 381 | } 382 | } 383 | 384 | echo CHtml::openTag('li'); 385 | echo CHtml::encode($category->title); 386 | $level=$category->level; 387 | } 388 | 389 | for($i=$level;$i;$i--) 390 | { 391 | echo CHtml::closeTag('li')."\n"; 392 | echo CHtml::closeTag('ul')."\n"; 393 | } 394 | ~~~ 395 | -------------------------------------------------------------------------------- /tests/unit/NestedSetBehaviorTest.php: -------------------------------------------------------------------------------- 1 | findByPk(1); 13 | $this->assertTrue($nestedSet instanceof NestedSet); 14 | $descendants=$nestedSet->descendants()->findAll(); 15 | $this->assertEquals(count($descendants),6); 16 | foreach($descendants as $descendant) 17 | $this->assertTrue($descendant instanceof NestedSet); 18 | $this->assertEquals($descendants[0]->primaryKey,2); 19 | $this->assertEquals($descendants[1]->primaryKey,3); 20 | $this->assertEquals($descendants[2]->primaryKey,4); 21 | $this->assertEquals($descendants[3]->primaryKey,5); 22 | $this->assertEquals($descendants[4]->primaryKey,6); 23 | $this->assertEquals($descendants[5]->primaryKey,7); 24 | 25 | // many roots 26 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(1); 27 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 28 | $descendants=$nestedSet->descendants()->findAll(); 29 | $this->assertEquals(count($descendants),6); 30 | foreach($descendants as $descendant) 31 | $this->assertTrue($descendant instanceof NestedSetWithManyRoots); 32 | $this->assertEquals($descendants[0]->primaryKey,2); 33 | $this->assertEquals($descendants[1]->primaryKey,3); 34 | $this->assertEquals($descendants[2]->primaryKey,4); 35 | $this->assertEquals($descendants[3]->primaryKey,5); 36 | $this->assertEquals($descendants[4]->primaryKey,6); 37 | $this->assertEquals($descendants[5]->primaryKey,7); 38 | } 39 | 40 | public function testChildren() 41 | { 42 | // single root 43 | $nestedSet=NestedSet::model()->findByPk(1); 44 | $this->assertTrue($nestedSet instanceof NestedSet); 45 | $children=$nestedSet->children()->findAll(); 46 | $this->assertEquals(count($children),2); 47 | foreach($children as $child) 48 | $this->assertTrue($child instanceof NestedSet); 49 | $this->assertEquals($children[0]->primaryKey,2); 50 | $this->assertEquals($children[1]->primaryKey,5); 51 | 52 | // many roots 53 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(1); 54 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 55 | $children=$nestedSet->children()->findAll(); 56 | $this->assertEquals(count($children),2); 57 | foreach($children as $child) 58 | $this->assertTrue($child instanceof NestedSetWithManyRoots); 59 | $this->assertEquals($children[0]->primaryKey,2); 60 | $this->assertEquals($children[1]->primaryKey,5); 61 | } 62 | 63 | public function testAncestors() 64 | { 65 | // single root 66 | $nestedSet=NestedSet::model()->findByPk(7); 67 | $this->assertTrue($nestedSet instanceof NestedSet); 68 | $ancestors=$nestedSet->ancestors()->findAll(); 69 | $this->assertEquals(count($ancestors),2); 70 | foreach($ancestors as $ancestor) 71 | $this->assertTrue($ancestor instanceof NestedSet); 72 | $this->assertEquals($ancestors[0]->primaryKey,1); 73 | $this->assertEquals($ancestors[1]->primaryKey,5); 74 | 75 | // many roots 76 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(7); 77 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 78 | $ancestors=$nestedSet->ancestors()->findAll(); 79 | $this->assertEquals(count($ancestors),2); 80 | foreach($ancestors as $ancestor) 81 | $this->assertTrue($ancestor instanceof NestedSetWithManyRoots); 82 | $this->assertEquals($ancestors[0]->primaryKey,1); 83 | $this->assertEquals($ancestors[1]->primaryKey,5); 84 | } 85 | 86 | public function testRoots() 87 | { 88 | // single root 89 | $roots=NestedSet::model()->roots()->findAll(); 90 | $this->assertEquals(count($roots),1); 91 | foreach($roots as $root) 92 | $this->assertTrue($root instanceof NestedSet); 93 | $this->assertEquals($roots[0]->primaryKey,1); 94 | 95 | // many roots 96 | $roots=NestedSetWithManyRoots::model()->roots()->findAll(); 97 | $this->assertEquals(count($roots),2); 98 | foreach($roots as $root) 99 | $this->assertTrue($root instanceof NestedSetWithManyRoots); 100 | $this->assertEquals($roots[0]->primaryKey,1); 101 | $this->assertEquals($roots[1]->primaryKey,8); 102 | } 103 | 104 | public function testParent() 105 | { 106 | // single root 107 | $nestedSet=NestedSet::model()->findByPk(4); 108 | $this->assertTrue($nestedSet instanceof NestedSet); 109 | $parent=$nestedSet->parent()->find(); 110 | $this->assertTrue($parent instanceof NestedSet); 111 | $this->assertEquals($parent->primaryKey,2); 112 | 113 | // many roots 114 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(4); 115 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 116 | $parent=$nestedSet->parent()->find(); 117 | $this->assertTrue($parent instanceof NestedSetWithManyRoots); 118 | $this->assertEquals($parent->primaryKey,2); 119 | } 120 | 121 | public function testPrev() 122 | { 123 | // single root 124 | $nestedSet=NestedSet::model()->findByPk(7); 125 | $this->assertTrue($nestedSet instanceof NestedSet); 126 | $sibling=$nestedSet->prev()->find(); 127 | $this->assertTrue($sibling instanceof NestedSet); 128 | $this->assertEquals($sibling->primaryKey,6); 129 | $sibling=$sibling->prev()->find(); 130 | $this->assertNull($sibling); 131 | 132 | // many roots 133 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(7); 134 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 135 | $sibling=$nestedSet->prev()->find(); 136 | $this->assertTrue($sibling instanceof NestedSetWithManyRoots); 137 | $this->assertEquals($sibling->primaryKey,6); 138 | $sibling=$sibling->prev()->find(); 139 | $this->assertNull($sibling); 140 | } 141 | 142 | public function testNext() 143 | { 144 | // single root 145 | $nestedSet=NestedSet::model()->findByPk(6); 146 | $this->assertTrue($nestedSet instanceof NestedSet); 147 | $sibling=$nestedSet->next()->find(); 148 | $this->assertTrue($sibling instanceof NestedSet); 149 | $this->assertEquals($sibling->primaryKey,7); 150 | $sibling=$sibling->next()->find(); 151 | $this->assertNull($sibling); 152 | 153 | // many roots 154 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(6); 155 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 156 | $sibling=$nestedSet->next()->find(); 157 | $this->assertTrue($sibling instanceof NestedSetWithManyRoots); 158 | $this->assertEquals($sibling->primaryKey,7); 159 | $sibling=$sibling->next()->find(); 160 | $this->assertNull($sibling); 161 | } 162 | 163 | /** 164 | * @depends testDescendants 165 | */ 166 | public function testIsDescendantOf() 167 | { 168 | // single root 169 | $nestedSet=NestedSet::model()->findByPk(1); 170 | $this->assertTrue($nestedSet instanceof NestedSet); 171 | $descendants=$nestedSet->descendants()->findAll(); 172 | foreach($descendants as $descendant) 173 | $this->assertTrue($descendant->isDescendantOf($nestedSet)); 174 | $descendant=NestedSet::model()->findByPk(4); 175 | $this->assertTrue($descendant instanceof NestedSet); 176 | $this->assertFalse($nestedSet->isDescendantOf($descendant)); 177 | 178 | // many roots 179 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(1); 180 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 181 | $descendants=$nestedSet->descendants()->findAll(); 182 | foreach($descendants as $descendant) 183 | $this->assertTrue($descendant->isDescendantOf($nestedSet)); 184 | $descendant=NestedSetWithManyRoots::model()->findByPk(4); 185 | $this->assertTrue($descendant instanceof NestedSetWithManyRoots); 186 | $this->assertFalse($nestedSet->isDescendantOf($descendant)); 187 | } 188 | 189 | public function testIsRoot() 190 | { 191 | // single root 192 | $roots=NestedSet::model()->roots()->findAll(); 193 | $this->assertEquals(count($roots),1); 194 | foreach($roots as $root) 195 | { 196 | $this->assertTrue($root instanceof NestedSet); 197 | $this->assertTrue($root->isRoot()); 198 | } 199 | $notRoot=NestedSet::model()->findByPk(4); 200 | $this->assertTrue($notRoot instanceof NestedSet); 201 | $this->assertFalse($notRoot->isRoot()); 202 | 203 | // many roots 204 | $roots=NestedSetWithManyRoots::model()->roots()->findAll(); 205 | $this->assertEquals(count($roots),2); 206 | foreach($roots as $root) 207 | { 208 | $this->assertTrue($root instanceof NestedSetWithManyRoots); 209 | $this->assertTrue($root->isRoot()); 210 | } 211 | $notRoot=NestedSetWithManyRoots::model()->findByPk(4); 212 | $this->assertTrue($notRoot instanceof NestedSetWithManyRoots); 213 | $this->assertFalse($notRoot->isRoot()); 214 | } 215 | 216 | public function testIsLeaf() 217 | { 218 | // single root 219 | $nestedSet=NestedSet::model()->findByPk(5); 220 | $this->assertTrue($nestedSet instanceof NestedSet); 221 | $this->assertFalse($nestedSet->isLeaf()); 222 | $descendants=$nestedSet->descendants()->findAll(); 223 | $this->assertEquals(count($descendants),2); 224 | foreach($descendants as $descendant) 225 | { 226 | $this->assertTrue($descendant instanceof NestedSet); 227 | $this->assertTrue($descendant->isLeaf()); 228 | } 229 | 230 | // many roots 231 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(5); 232 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 233 | $this->assertFalse($nestedSet->isLeaf()); 234 | $descendants=$nestedSet->descendants()->findAll(); 235 | $this->assertEquals(count($descendants),2); 236 | foreach($descendants as $descendant) 237 | { 238 | $this->assertTrue($descendant instanceof NestedSetWithManyRoots); 239 | $this->assertTrue($descendant->isLeaf()); 240 | } 241 | } 242 | 243 | public function testSaveNode() 244 | { 245 | // single root 246 | 247 | // many roots 248 | $nestedSet=new NestedSetWithManyRoots; 249 | $this->assertFalse($nestedSet->saveNode()); 250 | $nestedSet->name='test'; 251 | $this->assertTrue($nestedSet->saveNode()); 252 | $this->assertEquals($nestedSet->root,$nestedSet->primaryKey); 253 | $this->assertEquals($nestedSet->lft,1); 254 | $this->assertEquals($nestedSet->rgt,2); 255 | $this->assertEquals($nestedSet->level,1); 256 | } 257 | 258 | public function testDeleteNode() 259 | { 260 | // single root 261 | $array=NestedSet::model()->findAll(); 262 | $nestedSet=NestedSet::model()->findByPk(4); 263 | $this->assertTrue($nestedSet instanceof NestedSet); 264 | $this->assertTrue($nestedSet->deleteNode()); 265 | $this->assertTrue($this->checkTree()); 266 | $this->assertTrue($nestedSet->getIsDeletedRecord()); 267 | $this->assertTrue($this->checkArray($array)); 268 | $nestedSet=NestedSet::model()->findByPk(5); 269 | $this->assertTrue($nestedSet instanceof NestedSet); 270 | $this->assertTrue($nestedSet->deleteNode()); 271 | $this->assertTrue($this->checkTree()); 272 | $this->assertTrue($nestedSet->getIsDeletedRecord()); 273 | $this->assertTrue($this->checkArray($array)); 274 | foreach($array as $item) 275 | { 276 | if(in_array($item->primaryKey,array(4,5,6,7))) 277 | $this->assertTrue($item->getIsDeletedRecord()); 278 | else 279 | $this->assertFalse($item->getIsDeletedRecord()); 280 | } 281 | 282 | // many roots 283 | $array=NestedSetWithManyRoots::model()->findAll(); 284 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(4); 285 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 286 | $this->assertTrue($nestedSet->deleteNode()); 287 | $this->assertTrue($this->checkTreeWithManyRoots()); 288 | $this->assertTrue($nestedSet->getIsDeletedRecord()); 289 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 290 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(9); 291 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 292 | $this->assertTrue($nestedSet->deleteNode()); 293 | $this->assertTrue($this->checkTreeWithManyRoots()); 294 | $this->assertTrue($nestedSet->getIsDeletedRecord()); 295 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 296 | foreach($array as $item) 297 | { 298 | if(in_array($item->primaryKey,array(4,9,10,11))) 299 | $this->assertTrue($item->getIsDeletedRecord()); 300 | else 301 | $this->assertFalse($item->getIsDeletedRecord()); 302 | } 303 | } 304 | 305 | public function testPrependTo() 306 | { 307 | // single root 308 | $array=NestedSet::model()->findAll(); 309 | $target=NestedSet::model()->findByPk(5); 310 | $this->assertTrue($target instanceof NestedSet); 311 | $nestedSet1=new NestedSet; 312 | $this->assertFalse($nestedSet1->prependTo($target)); 313 | $nestedSet1->name='test'; 314 | $this->assertTrue($nestedSet1->prependTo($target)); 315 | $this->assertTrue($this->checkTree()); 316 | $array[]=$nestedSet1; 317 | $nestedSet2=new NestedSet; 318 | $nestedSet2->name='test'; 319 | $this->assertTrue($nestedSet2->prependTo($target)); 320 | $this->assertTrue($this->checkTree()); 321 | $array[]=$nestedSet2; 322 | $this->assertTrue($this->checkArray($array)); 323 | 324 | // many roots 325 | $array=NestedSetWithManyRoots::model()->findAll(); 326 | $target=NestedSetWithManyRoots::model()->findByPk(5); 327 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 328 | $nestedSet1=new NestedSetWithManyRoots; 329 | $this->assertFalse($nestedSet1->prependTo($target)); 330 | $nestedSet1->name='test'; 331 | $this->assertTrue($nestedSet1->prependTo($target)); 332 | $this->assertTrue($this->checkTreeWithManyRoots()); 333 | $array[]=$nestedSet1; 334 | $nestedSet2=new NestedSetWithManyRoots; 335 | $nestedSet2->name='test'; 336 | $this->assertTrue($nestedSet2->prependTo($target)); 337 | $this->assertTrue($this->checkTreeWithManyRoots()); 338 | $array[]=$nestedSet2; 339 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 340 | } 341 | 342 | public function testAppendTo() 343 | { 344 | // single root 345 | $array=NestedSet::model()->findAll(); 346 | $target=NestedSet::model()->findByPk(2); 347 | $this->assertTrue($target instanceof NestedSet); 348 | $nestedSet1=new NestedSet; 349 | $this->assertFalse($nestedSet1->appendTo($target)); 350 | $nestedSet1->name='test'; 351 | $this->assertTrue($nestedSet1->appendTo($target)); 352 | $this->assertTrue($this->checkTree()); 353 | $array[]=$nestedSet1; 354 | $nestedSet2=new NestedSet; 355 | $nestedSet2->name='test'; 356 | $this->assertTrue($nestedSet2->appendTo($target)); 357 | $this->assertTrue($this->checkTree()); 358 | $array[]=$nestedSet2; 359 | $this->assertTrue($this->checkArray($array)); 360 | 361 | // many roots 362 | $array=NestedSetWithManyRoots::model()->findAll(); 363 | $target=NestedSetWithManyRoots::model()->findByPk(2); 364 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 365 | $nestedSet1=new NestedSetWithManyRoots; 366 | $this->assertFalse($nestedSet1->appendTo($target)); 367 | $nestedSet1->name='test'; 368 | $this->assertTrue($nestedSet1->appendTo($target)); 369 | $this->assertTrue($this->checkTreeWithManyRoots()); 370 | $array[]=$nestedSet1; 371 | $nestedSet2=new NestedSetWithManyRoots; 372 | $nestedSet2->name='test'; 373 | $this->assertTrue($nestedSet2->appendTo($target)); 374 | $this->assertTrue($this->checkTreeWithManyRoots()); 375 | $array[]=$nestedSet2; 376 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 377 | } 378 | 379 | public function testInsertBefore() 380 | { 381 | // single root 382 | $array=NestedSet::model()->findAll(); 383 | $target=NestedSet::model()->findByPk(5); 384 | $this->assertTrue($target instanceof NestedSet); 385 | $nestedSet1=new NestedSet; 386 | $this->assertFalse($nestedSet1->insertBefore($target)); 387 | $nestedSet1->name='test'; 388 | $this->assertTrue($nestedSet1->insertBefore($target)); 389 | $this->assertTrue($this->checkTree()); 390 | $array[]=$nestedSet1; 391 | $nestedSet2=new NestedSet; 392 | $nestedSet2->name='test'; 393 | $this->assertTrue($nestedSet2->insertBefore($target)); 394 | $this->assertTrue($this->checkTree()); 395 | $array[]=$nestedSet2; 396 | $this->assertTrue($this->checkArray($array)); 397 | 398 | // many roots 399 | $array=NestedSetWithManyRoots::model()->findAll(); 400 | $target=NestedSetWithManyRoots::model()->findByPk(5); 401 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 402 | $nestedSet1=new NestedSetWithManyRoots; 403 | $this->assertFalse($nestedSet1->insertBefore($target)); 404 | $nestedSet1->name='test'; 405 | $this->assertTrue($nestedSet1->insertBefore($target)); 406 | $this->assertTrue($this->checkTreeWithManyRoots()); 407 | $array[]=$nestedSet1; 408 | $nestedSet2=new NestedSetWithManyRoots; 409 | $nestedSet2->name='test'; 410 | $this->assertTrue($nestedSet2->insertBefore($target)); 411 | $this->assertTrue($this->checkTreeWithManyRoots()); 412 | $array[]=$nestedSet2; 413 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 414 | } 415 | 416 | public function testInsertAfter() 417 | { 418 | // single root 419 | $array=NestedSet::model()->findAll(); 420 | $target=NestedSet::model()->findByPk(2); 421 | $this->assertTrue($target instanceof NestedSet); 422 | $nestedSet1=new NestedSet; 423 | $this->assertFalse($nestedSet1->insertAfter($target)); 424 | $nestedSet1->name='test'; 425 | $this->assertTrue($nestedSet1->insertAfter($target)); 426 | $this->assertTrue($this->checkTree()); 427 | $array[]=$nestedSet1; 428 | $nestedSet2=new NestedSet; 429 | $nestedSet2->name='test'; 430 | $this->assertTrue($nestedSet2->insertAfter($target)); 431 | $this->assertTrue($this->checkTree()); 432 | $array[]=$nestedSet2; 433 | $this->assertTrue($this->checkArray($array)); 434 | 435 | // many roots 436 | $array=NestedSetWithManyRoots::model()->findAll(); 437 | $target=NestedSetWithManyRoots::model()->findByPk(2); 438 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 439 | $nestedSet1=new NestedSetWithManyRoots; 440 | $this->assertFalse($nestedSet1->insertAfter($target)); 441 | $nestedSet1->name='test'; 442 | $this->assertTrue($nestedSet1->insertAfter($target)); 443 | $this->assertTrue($this->checkTreeWithManyRoots()); 444 | $array[]=$nestedSet1; 445 | $nestedSet2=new NestedSetWithManyRoots; 446 | $nestedSet2->name='test'; 447 | $this->assertTrue($nestedSet2->insertAfter($target)); 448 | $this->assertTrue($this->checkTreeWithManyRoots()); 449 | $array[]=$nestedSet2; 450 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 451 | } 452 | 453 | public function testMoveBefore() 454 | { 455 | // single root 456 | $array=NestedSet::model()->findAll(); 457 | 458 | $nestedSet=NestedSet::model()->findByPk(6); 459 | $this->assertTrue($nestedSet instanceof NestedSet); 460 | $target=NestedSet::model()->findByPk(2); 461 | $this->assertTrue($target instanceof NestedSet); 462 | $this->assertTrue($nestedSet->moveBefore($target)); 463 | $this->assertTrue($this->checkTree()); 464 | 465 | $this->assertTrue($this->checkArray($array)); 466 | 467 | $nestedSet=NestedSet::model()->findByPk(5); 468 | $this->assertTrue($nestedSet instanceof NestedSet); 469 | $this->assertTrue($nestedSet->moveBefore($target)); 470 | $this->assertTrue($this->checkTree()); 471 | 472 | $this->assertTrue($this->checkArray($array)); 473 | 474 | // many roots 475 | $array=NestedSetWithManyRoots::model()->findAll(); 476 | 477 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(6); 478 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 479 | $target=NestedSetWithManyRoots::model()->findByPk(2); 480 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 481 | $this->assertTrue($nestedSet->moveBefore($target)); 482 | $this->assertTrue($this->checkTreeWithManyRoots()); 483 | 484 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 485 | 486 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(5); 487 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 488 | $this->assertTrue($nestedSet->moveBefore($target)); 489 | $this->assertTrue($this->checkTreeWithManyRoots()); 490 | 491 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 492 | 493 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(6); 494 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 495 | $target=NestedSetWithManyRoots::model()->findByPk(9); 496 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 497 | $this->assertTrue($nestedSet->moveBefore($target)); 498 | $this->assertTrue($this->checkTreeWithManyRoots()); 499 | 500 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 501 | 502 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(5); 503 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 504 | $this->assertTrue($nestedSet->moveBefore($target)); 505 | $this->assertTrue($this->checkTreeWithManyRoots()); 506 | 507 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 508 | } 509 | 510 | public function testMoveAfter() 511 | { 512 | // single root 513 | $array=NestedSet::model()->findAll(); 514 | 515 | $nestedSet=NestedSet::model()->findByPk(3); 516 | $this->assertTrue($nestedSet instanceof NestedSet); 517 | $target=NestedSet::model()->findByPk(5); 518 | $this->assertTrue($target instanceof NestedSet); 519 | $this->assertTrue($nestedSet->moveAfter($target)); 520 | $this->assertTrue($this->checkTree()); 521 | 522 | $this->assertTrue($this->checkArray($array)); 523 | 524 | $nestedSet=NestedSet::model()->findByPk(2); 525 | $this->assertTrue($nestedSet instanceof NestedSet); 526 | $this->assertTrue($nestedSet->moveAfter($target)); 527 | $this->assertTrue($this->checkTree()); 528 | 529 | $this->assertTrue($this->checkArray($array)); 530 | 531 | // many roots 532 | $array=NestedSetWithManyRoots::model()->findAll(); 533 | 534 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(3); 535 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 536 | $target=NestedSetWithManyRoots::model()->findByPk(5); 537 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 538 | $this->assertTrue($nestedSet->moveAfter($target)); 539 | $this->assertTrue($this->checkTreeWithManyRoots()); 540 | 541 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 542 | 543 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(2); 544 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 545 | $this->assertTrue($nestedSet->moveAfter($target)); 546 | $this->assertTrue($this->checkTreeWithManyRoots()); 547 | 548 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 549 | 550 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(3); 551 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 552 | $target=NestedSetWithManyRoots::model()->findByPk(12); 553 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 554 | $this->assertTrue($nestedSet->moveAfter($target)); 555 | $this->assertTrue($this->checkTreeWithManyRoots()); 556 | 557 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 558 | 559 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(2); 560 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 561 | $this->assertTrue($nestedSet->moveAfter($target)); 562 | $this->assertTrue($this->checkTreeWithManyRoots()); 563 | 564 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 565 | } 566 | 567 | public function testMoveAsFirst() 568 | { 569 | // single root 570 | $array=NestedSet::model()->findAll(); 571 | 572 | $nestedSet=NestedSet::model()->findByPk(6); 573 | $this->assertTrue($nestedSet instanceof NestedSet); 574 | $target=NestedSet::model()->findByPk(2); 575 | $this->assertTrue($target instanceof NestedSet); 576 | $this->assertTrue($nestedSet->moveAsFirst($target)); 577 | $this->assertTrue($this->checkTree()); 578 | 579 | $this->assertTrue($this->checkArray($array)); 580 | 581 | $nestedSet=NestedSet::model()->findByPk(5); 582 | $this->assertTrue($nestedSet instanceof NestedSet); 583 | $this->assertTrue($nestedSet->moveAsFirst($target)); 584 | $this->assertTrue($this->checkTree()); 585 | 586 | $this->assertTrue($this->checkArray($array)); 587 | 588 | // many roots 589 | $array=NestedSetWithManyRoots::model()->findAll(); 590 | 591 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(6); 592 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 593 | $target=NestedSetWithManyRoots::model()->findByPk(2); 594 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 595 | $this->assertTrue($nestedSet->moveAsFirst($target)); 596 | $this->assertTrue($this->checkTreeWithManyRoots()); 597 | 598 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 599 | 600 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(5); 601 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 602 | $this->assertTrue($nestedSet->moveAsFirst($target)); 603 | $this->assertTrue($this->checkTreeWithManyRoots()); 604 | 605 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 606 | 607 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(6); 608 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 609 | $target=NestedSetWithManyRoots::model()->findByPk(9); 610 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 611 | $this->assertTrue($nestedSet->moveAsFirst($target)); 612 | $this->assertTrue($this->checkTreeWithManyRoots()); 613 | 614 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 615 | 616 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(5); 617 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 618 | $this->assertTrue($nestedSet->moveAsFirst($target)); 619 | $this->assertTrue($this->checkTreeWithManyRoots()); 620 | 621 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 622 | } 623 | 624 | public function testMoveAsLast() 625 | { 626 | // single root 627 | $array=NestedSet::model()->findAll(); 628 | 629 | $nestedSet=NestedSet::model()->findByPk(3); 630 | $this->assertTrue($nestedSet instanceof NestedSet); 631 | $target=NestedSet::model()->findByPk(5); 632 | $this->assertTrue($target instanceof NestedSet); 633 | $this->assertTrue($nestedSet->moveAsLast($target)); 634 | $this->assertTrue($this->checkTree()); 635 | 636 | $this->assertTrue($this->checkArray($array)); 637 | 638 | $nestedSet=NestedSet::model()->findByPk(2); 639 | $this->assertTrue($nestedSet instanceof NestedSet); 640 | $this->assertTrue($nestedSet->moveAsLast($target)); 641 | $this->assertTrue($this->checkTree()); 642 | 643 | $this->assertTrue($this->checkArray($array)); 644 | 645 | // many roots 646 | $array=NestedSetWithManyRoots::model()->findAll(); 647 | 648 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(3); 649 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 650 | $target=NestedSetWithManyRoots::model()->findByPk(5); 651 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 652 | $this->assertTrue($nestedSet->moveAsLast($target)); 653 | $this->assertTrue($this->checkTreeWithManyRoots()); 654 | 655 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 656 | 657 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(2); 658 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 659 | $this->assertTrue($nestedSet->moveAsLast($target)); 660 | $this->assertTrue($this->checkTreeWithManyRoots()); 661 | 662 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 663 | 664 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(3); 665 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 666 | $target=NestedSetWithManyRoots::model()->findByPk(12); 667 | $this->assertTrue($target instanceof NestedSetWithManyRoots); 668 | $this->assertTrue($nestedSet->moveAsLast($target)); 669 | $this->assertTrue($this->checkTreeWithManyRoots()); 670 | 671 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 672 | 673 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(2); 674 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 675 | $this->assertTrue($nestedSet->moveAsLast($target)); 676 | $this->assertTrue($this->checkTreeWithManyRoots()); 677 | 678 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 679 | } 680 | 681 | public function testMoveAsRoot() 682 | { 683 | $array=NestedSetWithManyRoots::model()->findAll(); 684 | 685 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(2); 686 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 687 | $this->assertTrue($nestedSet->moveAsRoot()); 688 | $this->assertTrue($this->checkTreeWithManyRoots()); 689 | 690 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 691 | 692 | $nestedSet=NestedSetWithManyRoots::model()->findByPk(10); 693 | $this->assertTrue($nestedSet instanceof NestedSetWithManyRoots); 694 | $this->assertTrue($nestedSet->moveAsRoot()); 695 | $this->assertTrue($this->checkTreeWithManyRoots()); 696 | 697 | $this->assertTrue($this->checkArrayWithManyRoots($array)); 698 | } 699 | 700 | private function checkTree() 701 | { 702 | return $this->checkTree1() 703 | && $this->checkTree2() 704 | && $this->checkTree3() 705 | && $this->checkTree4(); 706 | } 707 | 708 | private function checkTree1() 709 | { 710 | return !Yii::app()->db->createCommand('SELECT COUNT(`id`) FROM `NestedSet` WHERE `lft`>=`rgt`;')->queryScalar(); 711 | } 712 | 713 | private function checkTree2() 714 | { 715 | return !Yii::app()->db->createCommand('SELECT COUNT(`id`) FROM `NestedSet` WHERE NOT MOD(`rgt`-`lft`,2);')->queryScalar(); 716 | } 717 | 718 | private function checkTree3() 719 | { 720 | return !Yii::app()->db->createCommand('SELECT COUNT(`id`) FROM `NestedSet` WHERE MOD(`lft`-`level`,2);')->queryScalar(); 721 | } 722 | 723 | private function checkTree4() 724 | { 725 | $row=Yii::app()->db->createCommand('SELECT MIN(`lft`),MAX(`rgt`),COUNT(`id`) FROM `NestedSet`;')->queryRow(false); 726 | 727 | if($row[0]!=1 || $row[1]!=$row[2]*2) 728 | return false; 729 | 730 | return true; 731 | } 732 | 733 | private function checkArray($array) 734 | { 735 | return $this->checkArray1($array) 736 | && $this->checkArray2($array) 737 | && $this->checkArray3($array) 738 | && $this->checkArray4($array); 739 | } 740 | 741 | private function checkArray1($array) 742 | { 743 | foreach($array as $node) 744 | { 745 | if(!$node->getIsDeletedRecord() && $node->lft>=$node->rgt) 746 | return false; 747 | } 748 | 749 | return true; 750 | } 751 | 752 | private function checkArray2($array) 753 | { 754 | foreach($array as $node) 755 | { 756 | if(!$node->getIsDeletedRecord() && !(($node->rgt-$node->lft)%2)) 757 | return false; 758 | } 759 | 760 | return true; 761 | } 762 | 763 | private function checkArray3($array) 764 | { 765 | foreach($array as $node) 766 | { 767 | if(!$node->getIsDeletedRecord() && ($node->lft-$node->level)%2) 768 | return false; 769 | } 770 | 771 | return true; 772 | } 773 | 774 | private function checkArray4($array) 775 | { 776 | $count=0; 777 | 778 | foreach($array as $node) 779 | { 780 | if($node->getIsDeletedRecord()) 781 | continue; 782 | else 783 | $count++; 784 | 785 | if(!isset($min) || $min>$node->lft) 786 | $min=$node->lft; 787 | 788 | if(!isset($max) || $max<$node->rgt) 789 | $max=$node->rgt; 790 | } 791 | 792 | if(!$count) 793 | return true; 794 | 795 | if($min!=1 || $max!=$count*2) 796 | return false; 797 | 798 | return true; 799 | } 800 | 801 | private function checkTreeWithManyRoots() 802 | { 803 | return $this->checkTreeWithManyRoots1() 804 | && $this->checkTreeWithManyRoots2() 805 | && $this->checkTreeWithManyRoots3() 806 | && $this->checkTreeWithManyRoots4(); 807 | } 808 | 809 | private function checkTreeWithManyRoots1() 810 | { 811 | return !Yii::app()->db->createCommand('SELECT COUNT(`id`) FROM `NestedSetWithManyRoots` WHERE `lft`>=`rgt` GROUP BY `root`;')->query()->getRowCount(); 812 | } 813 | 814 | private function checkTreeWithManyRoots2() 815 | { 816 | return !Yii::app()->db->createCommand('SELECT COUNT(`id`) FROM `NestedSetWithManyRoots` WHERE NOT MOD(`rgt`-`lft`,2) GROUP BY `root`;')->query()->getRowCount(); 817 | } 818 | 819 | private function checkTreeWithManyRoots3() 820 | { 821 | return !Yii::app()->db->createCommand('SELECT COUNT(`id`) FROM `NestedSetWithManyRoots` WHERE MOD(`lft`-`level`,2) GROUP BY `root`;')->query()->getRowCount(); 822 | } 823 | 824 | private function checkTreeWithManyRoots4() 825 | { 826 | $rows=Yii::app()->db->createCommand('SELECT MIN(`lft`),MAX(`rgt`),COUNT(`id`) FROM `NestedSetWithManyRoots` GROUP BY `root`;')->queryAll(false); 827 | 828 | foreach($rows as $row) 829 | { 830 | if($row[0]!=1 || $row[1]!=$row[2]*2) 831 | return false; 832 | } 833 | 834 | return true; 835 | } 836 | 837 | private function checkArrayWithManyRoots($array) 838 | { 839 | return $this->checkArrayWithManyRoots1($array) 840 | && $this->checkArrayWithManyRoots2($array) 841 | && $this->checkArrayWithManyRoots3($array) 842 | && $this->checkArrayWithManyRoots4($array); 843 | } 844 | 845 | private function checkArrayWithManyRoots1($array) 846 | { 847 | foreach($array as $node) 848 | { 849 | if(!$node->getIsDeletedRecord() && $node->lft>=$node->rgt) 850 | return false; 851 | } 852 | 853 | return true; 854 | } 855 | 856 | private function checkArrayWithManyRoots2($array) 857 | { 858 | foreach($array as $node) 859 | { 860 | if(!$node->getIsDeletedRecord() && !(($node->rgt-$node->lft)%2)) 861 | return false; 862 | } 863 | 864 | return true; 865 | } 866 | 867 | private function checkArrayWithManyRoots3($array) 868 | { 869 | foreach($array as $node) 870 | { 871 | if(!$node->getIsDeletedRecord() && ($node->lft-$node->level)%2) 872 | return false; 873 | } 874 | 875 | return true; 876 | } 877 | 878 | private function checkArrayWithManyRoots4($array) 879 | { 880 | $min=array(); 881 | $max=array(); 882 | $count=array(); 883 | 884 | foreach($array as $n=>$node) 885 | { 886 | if($node->getIsDeletedRecord()) 887 | continue; 888 | else if(isset($count[$node->root])) 889 | $count[$node->root]++; 890 | else 891 | $count[$node->root]=1; 892 | 893 | if(!isset($min[$node->root]) || $min[$node->root]>$node->lft) 894 | $min[$node->root]=$node->lft; 895 | 896 | if(!isset($max[$node->root]) || $max[$node->root]<$node->rgt) 897 | $max[$node->root]=$node->rgt; 898 | } 899 | 900 | foreach($count as $root=>$c) 901 | { 902 | if($min[$root]!=1 || $max[$root]!=$c*2) 903 | return false; 904 | } 905 | 906 | return true; 907 | } 908 | } -------------------------------------------------------------------------------- /NestedSetBehavior.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://github.com/yiiext/nested-set-behavior 7 | */ 8 | 9 | /** 10 | * Provides nested set functionality for a model. 11 | * 12 | * @version 1.06 13 | * @package yiiext.behaviors.model.trees 14 | */ 15 | class NestedSetBehavior extends CActiveRecordBehavior 16 | { 17 | public $hasManyRoots=false; 18 | public $rootAttribute='root'; 19 | public $leftAttribute='lft'; 20 | public $rightAttribute='rgt'; 21 | public $levelAttribute='level'; 22 | private $_ignoreEvent=false; 23 | private $_deleted=false; 24 | private $_id; 25 | private static $_cached; 26 | private static $_c=0; 27 | 28 | /** 29 | * Named scope. Gets descendants for node. 30 | * @param int $depth the depth. 31 | * @return CActiveRecord the owner. 32 | */ 33 | public function descendants($depth=null) 34 | { 35 | $owner=$this->getOwner(); 36 | $db=$owner->getDbConnection(); 37 | $criteria=$owner->getDbCriteria(); 38 | $alias=$db->quoteColumnName($owner->getTableAlias()); 39 | 40 | $criteria->mergeWith(array( 41 | 'condition'=>$alias.'.'.$db->quoteColumnName($this->leftAttribute).'>'.$owner->{$this->leftAttribute}. 42 | ' AND '.$alias.'.'.$db->quoteColumnName($this->rightAttribute).'<'.$owner->{$this->rightAttribute}, 43 | 'order'=>$alias.'.'.$db->quoteColumnName($this->leftAttribute), 44 | )); 45 | 46 | if($depth!==null) 47 | $criteria->addCondition($alias.'.'.$db->quoteColumnName($this->levelAttribute).'<='.($owner->{$this->levelAttribute}+$depth)); 48 | 49 | if($this->hasManyRoots) 50 | { 51 | $criteria->addCondition($alias.'.'.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount); 52 | $criteria->params[CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++]=$owner->{$this->rootAttribute}; 53 | } 54 | 55 | return $owner; 56 | } 57 | 58 | /** 59 | * Named scope. Gets children for node (direct descendants only). 60 | * @return CActiveRecord the owner. 61 | */ 62 | public function children() 63 | { 64 | return $this->descendants(1); 65 | } 66 | 67 | /** 68 | * Named scope. Gets ancestors for node. 69 | * @param int $depth the depth. 70 | * @return CActiveRecord the owner. 71 | */ 72 | public function ancestors($depth=null) 73 | { 74 | $owner=$this->getOwner(); 75 | $db=$owner->getDbConnection(); 76 | $criteria=$owner->getDbCriteria(); 77 | $alias=$db->quoteColumnName($owner->getTableAlias()); 78 | 79 | $criteria->mergeWith(array( 80 | 'condition'=>$alias.'.'.$db->quoteColumnName($this->leftAttribute).'<'.$owner->{$this->leftAttribute}. 81 | ' AND '.$alias.'.'.$db->quoteColumnName($this->rightAttribute).'>'.$owner->{$this->rightAttribute}, 82 | 'order'=>$alias.'.'.$db->quoteColumnName($this->leftAttribute), 83 | )); 84 | 85 | if($depth!==null) 86 | $criteria->addCondition($alias.'.'.$db->quoteColumnName($this->levelAttribute).'>='.($owner->{$this->levelAttribute}-$depth)); 87 | 88 | if($this->hasManyRoots) 89 | { 90 | $criteria->addCondition($alias.'.'.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount); 91 | $criteria->params[CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++]=$owner->{$this->rootAttribute}; 92 | } 93 | 94 | return $owner; 95 | } 96 | 97 | /** 98 | * Named scope. Gets root node(s). 99 | * @return CActiveRecord the owner. 100 | */ 101 | public function roots() 102 | { 103 | $owner=$this->getOwner(); 104 | $db=$owner->getDbConnection(); 105 | $owner->getDbCriteria()->addCondition($db->quoteColumnName($owner->getTableAlias()).'.'.$db->quoteColumnName($this->leftAttribute).'=1'); 106 | 107 | return $owner; 108 | } 109 | 110 | /** 111 | * Named scope. Gets parent of node. 112 | * @return CActiveRecord the owner. 113 | */ 114 | public function parent() 115 | { 116 | $owner=$this->getOwner(); 117 | $db=$owner->getDbConnection(); 118 | $criteria=$owner->getDbCriteria(); 119 | $alias=$db->quoteColumnName($owner->getTableAlias()); 120 | 121 | $criteria->mergeWith(array( 122 | 'condition'=>$alias.'.'.$db->quoteColumnName($this->leftAttribute).'<'.$owner->{$this->leftAttribute}. 123 | ' AND '.$alias.'.'.$db->quoteColumnName($this->rightAttribute).'>'.$owner->{$this->rightAttribute}, 124 | 'order'=>$alias.'.'.$db->quoteColumnName($this->rightAttribute), 125 | )); 126 | 127 | if($this->hasManyRoots) 128 | { 129 | $criteria->addCondition($alias.'.'.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount); 130 | $criteria->params[CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++]=$owner->{$this->rootAttribute}; 131 | } 132 | 133 | return $owner; 134 | } 135 | 136 | /** 137 | * Named scope. Gets previous sibling of node. 138 | * @return CActiveRecord the owner. 139 | */ 140 | public function prev() 141 | { 142 | $owner=$this->getOwner(); 143 | $db=$owner->getDbConnection(); 144 | $criteria=$owner->getDbCriteria(); 145 | $alias=$db->quoteColumnName($owner->getTableAlias()); 146 | $criteria->addCondition($alias.'.'.$db->quoteColumnName($this->rightAttribute).'='.($owner->{$this->leftAttribute}-1)); 147 | 148 | if($this->hasManyRoots) 149 | { 150 | $criteria->addCondition($alias.'.'.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount); 151 | $criteria->params[CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++]=$owner->{$this->rootAttribute}; 152 | } 153 | 154 | return $owner; 155 | } 156 | 157 | /** 158 | * Named scope. Gets next sibling of node. 159 | * @return CActiveRecord the owner. 160 | */ 161 | public function next() 162 | { 163 | $owner=$this->getOwner(); 164 | $db=$owner->getDbConnection(); 165 | $criteria=$owner->getDbCriteria(); 166 | $alias=$db->quoteColumnName($owner->getTableAlias()); 167 | $criteria->addCondition($alias.'.'.$db->quoteColumnName($this->leftAttribute).'='.($owner->{$this->rightAttribute}+1)); 168 | 169 | if($this->hasManyRoots) 170 | { 171 | $criteria->addCondition($alias.'.'.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount); 172 | $criteria->params[CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++]=$owner->{$this->rootAttribute}; 173 | } 174 | 175 | return $owner; 176 | } 177 | 178 | /** 179 | * Create root node if multiple-root tree mode. Update node if it's not new. 180 | * @param boolean $runValidation whether to perform validation. 181 | * @param boolean $attributes list of attributes. 182 | * @return boolean whether the saving succeeds. 183 | */ 184 | public function save($runValidation=true,$attributes=null) 185 | { 186 | $owner=$this->getOwner(); 187 | 188 | if($runValidation && !$owner->validate($attributes)) 189 | return false; 190 | 191 | if($owner->getIsNewRecord()) 192 | return $this->makeRoot($attributes); 193 | 194 | $this->_ignoreEvent=true; 195 | $result=$owner->update($attributes); 196 | $this->_ignoreEvent=false; 197 | 198 | return $result; 199 | } 200 | 201 | /** 202 | * Create root node if multiple-root tree mode. Update node if it's not new. 203 | * @param boolean $runValidation whether to perform validation. 204 | * @param boolean $attributes list of attributes. 205 | * @return boolean whether the saving succeeds. 206 | */ 207 | public function saveNode($runValidation=true,$attributes=null) 208 | { 209 | return $this->save($runValidation,$attributes); 210 | } 211 | 212 | /** 213 | * Deletes node and it's descendants. 214 | * @return boolean whether the deletion is successful. 215 | * @throws CDbException 216 | * @throws Exception 217 | */ 218 | public function delete() 219 | { 220 | $owner=$this->getOwner(); 221 | 222 | if($owner->getIsNewRecord()) 223 | throw new CDbException(Yii::t('yiiext','The node cannot be deleted because it is new.')); 224 | 225 | if($this->getIsDeletedRecord()) 226 | throw new CDbException(Yii::t('yiiext','The node cannot be deleted because it is already deleted.')); 227 | 228 | $db=$owner->getDbConnection(); 229 | 230 | if($db->getCurrentTransaction()===null) 231 | $transaction=$db->beginTransaction(); 232 | 233 | try 234 | { 235 | if($owner->isLeaf()) 236 | { 237 | $this->_ignoreEvent=true; 238 | $result=$owner->delete(); 239 | $this->_ignoreEvent=false; 240 | } 241 | else 242 | { 243 | $condition=$db->quoteColumnName($this->leftAttribute).'>='.$owner->{$this->leftAttribute}.' AND '. 244 | $db->quoteColumnName($this->rightAttribute).'<='.$owner->{$this->rightAttribute}; 245 | 246 | $params=array(); 247 | 248 | if($this->hasManyRoots) 249 | { 250 | $condition.=' AND '.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount; 251 | $params[CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++]=$owner->{$this->rootAttribute}; 252 | } 253 | 254 | $result=$owner->deleteAll($condition,$params)>0; 255 | } 256 | 257 | if(!$result) 258 | { 259 | if(isset($transaction)) 260 | $transaction->rollback(); 261 | 262 | return false; 263 | } 264 | 265 | $this->shiftLeftRight($owner->{$this->rightAttribute}+1,$owner->{$this->leftAttribute}-$owner->{$this->rightAttribute}-1); 266 | 267 | if(isset($transaction)) 268 | $transaction->commit(); 269 | 270 | $this->correctCachedOnDelete(); 271 | } 272 | catch(Exception $e) 273 | { 274 | if(isset($transaction)) 275 | $transaction->rollback(); 276 | 277 | throw $e; 278 | } 279 | 280 | return true; 281 | } 282 | 283 | /** 284 | * Deletes node and it's descendants. 285 | * @return boolean whether the deletion is successful. 286 | */ 287 | public function deleteNode() 288 | { 289 | return $this->delete(); 290 | } 291 | 292 | /** 293 | * Prepends node to target as first child. 294 | * @param CActiveRecord $target the target. 295 | * @param boolean $runValidation whether to perform validation. 296 | * @param array $attributes list of attributes. 297 | * @return boolean whether the prepending succeeds. 298 | */ 299 | public function prependTo($target,$runValidation=true,$attributes=null) 300 | { 301 | return $this->addNode($target,$target->{$this->leftAttribute}+1,1,$runValidation,$attributes); 302 | } 303 | 304 | /** 305 | * Prepends target to node as first child. 306 | * @param CActiveRecord $target the target. 307 | * @param boolean $runValidation whether to perform validation. 308 | * @param array $attributes list of attributes. 309 | * @return boolean whether the prepending succeeds. 310 | */ 311 | public function prepend($target,$runValidation=true,$attributes=null) 312 | { 313 | return $target->prependTo($this->getOwner(),$runValidation,$attributes); 314 | } 315 | 316 | /** 317 | * Appends node to target as last child. 318 | * @param CActiveRecord $target the target. 319 | * @param boolean $runValidation whether to perform validation. 320 | * @param array $attributes list of attributes. 321 | * @return boolean whether the appending succeeds. 322 | */ 323 | public function appendTo($target,$runValidation=true,$attributes=null) 324 | { 325 | return $this->addNode($target,$target->{$this->rightAttribute},1,$runValidation,$attributes); 326 | } 327 | 328 | /** 329 | * Appends target to node as last child. 330 | * @param CActiveRecord $target the target. 331 | * @param boolean $runValidation whether to perform validation. 332 | * @param array $attributes list of attributes. 333 | * @return boolean whether the appending succeeds. 334 | */ 335 | public function append($target,$runValidation=true,$attributes=null) 336 | { 337 | return $target->appendTo($this->getOwner(),$runValidation,$attributes); 338 | } 339 | 340 | /** 341 | * Inserts node as previous sibling of target. 342 | * @param CActiveRecord $target the target. 343 | * @param boolean $runValidation whether to perform validation. 344 | * @param array $attributes list of attributes. 345 | * @return boolean whether the inserting succeeds. 346 | */ 347 | public function insertBefore($target,$runValidation=true,$attributes=null) 348 | { 349 | return $this->addNode($target,$target->{$this->leftAttribute},0,$runValidation,$attributes); 350 | } 351 | 352 | /** 353 | * Inserts node as next sibling of target. 354 | * @param CActiveRecord $target the target. 355 | * @param boolean $runValidation whether to perform validation. 356 | * @param array $attributes list of attributes. 357 | * @return boolean whether the inserting succeeds. 358 | */ 359 | public function insertAfter($target,$runValidation=true,$attributes=null) 360 | { 361 | return $this->addNode($target,$target->{$this->rightAttribute}+1,0,$runValidation,$attributes); 362 | } 363 | 364 | /** 365 | * Move node as previous sibling of target. 366 | * @param CActiveRecord $target the target. 367 | * @return boolean whether the moving succeeds. 368 | */ 369 | public function moveBefore($target) 370 | { 371 | return $this->moveNode($target,$target->{$this->leftAttribute},0); 372 | } 373 | 374 | /** 375 | * Move node as next sibling of target. 376 | * @param CActiveRecord $target the target. 377 | * @return boolean whether the moving succeeds. 378 | */ 379 | public function moveAfter($target) 380 | { 381 | return $this->moveNode($target,$target->{$this->rightAttribute}+1,0); 382 | } 383 | 384 | /** 385 | * Move node as first child of target. 386 | * @param CActiveRecord $target the target. 387 | * @return boolean whether the moving succeeds. 388 | */ 389 | public function moveAsFirst($target) 390 | { 391 | return $this->moveNode($target,$target->{$this->leftAttribute}+1,1); 392 | } 393 | 394 | /** 395 | * Move node as last child of target. 396 | * @param CActiveRecord $target the target. 397 | * @return boolean whether the moving succeeds. 398 | */ 399 | public function moveAsLast($target) 400 | { 401 | return $this->moveNode($target,$target->{$this->rightAttribute},1); 402 | } 403 | 404 | /** 405 | * Move node as new root. 406 | * @return boolean whether the moving succeeds. 407 | * @throws CDbException 408 | * @throws CException 409 | * @throws Exception 410 | */ 411 | public function moveAsRoot() 412 | { 413 | $owner=$this->getOwner(); 414 | 415 | if(!$this->hasManyRoots) 416 | throw new CException(Yii::t('yiiext','Many roots mode is off.')); 417 | 418 | if($owner->getIsNewRecord()) 419 | throw new CException(Yii::t('yiiext','The node should not be new record.')); 420 | 421 | if($this->getIsDeletedRecord()) 422 | throw new CDbException(Yii::t('yiiext','The node should not be deleted.')); 423 | 424 | if($owner->isRoot()) 425 | throw new CException(Yii::t('yiiext','The node already is root node.')); 426 | 427 | $db=$owner->getDbConnection(); 428 | 429 | if($db->getCurrentTransaction()===null) 430 | $transaction=$db->beginTransaction(); 431 | 432 | try 433 | { 434 | $left=$owner->{$this->leftAttribute}; 435 | $right=$owner->{$this->rightAttribute}; 436 | $levelDelta=1-$owner->{$this->levelAttribute}; 437 | $delta=1-$left; 438 | 439 | $owner->updateAll( 440 | array( 441 | $this->leftAttribute=>new CDbExpression($db->quoteColumnName($this->leftAttribute).sprintf('%+d',$delta)), 442 | $this->rightAttribute=>new CDbExpression($db->quoteColumnName($this->rightAttribute).sprintf('%+d',$delta)), 443 | $this->levelAttribute=>new CDbExpression($db->quoteColumnName($this->levelAttribute).sprintf('%+d',$levelDelta)), 444 | $this->rootAttribute=>$owner->getPrimaryKey(), 445 | ), 446 | $db->quoteColumnName($this->leftAttribute).'>='.$left.' AND '. 447 | $db->quoteColumnName($this->rightAttribute).'<='.$right.' AND '. 448 | $db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount, 449 | array(CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++=>$owner->{$this->rootAttribute})); 450 | 451 | $this->shiftLeftRight($right+1,$left-$right-1); 452 | 453 | if(isset($transaction)) 454 | $transaction->commit(); 455 | 456 | $this->correctCachedOnMoveBetweenTrees(1,$levelDelta,$owner->getPrimaryKey()); 457 | } 458 | catch(Exception $e) 459 | { 460 | if(isset($transaction)) 461 | $transaction->rollback(); 462 | 463 | throw $e; 464 | } 465 | 466 | return true; 467 | } 468 | 469 | /** 470 | * Determines if node is descendant of subject node. 471 | * @param CActiveRecord $subj the subject node. 472 | * @return boolean whether the node is descendant of subject node. 473 | */ 474 | public function isDescendantOf($subj) 475 | { 476 | $owner=$this->getOwner(); 477 | $result=($owner->{$this->leftAttribute}>$subj->{$this->leftAttribute}) 478 | && ($owner->{$this->rightAttribute}<$subj->{$this->rightAttribute}); 479 | 480 | if($this->hasManyRoots) 481 | $result=$result && ($owner->{$this->rootAttribute}===$subj->{$this->rootAttribute}); 482 | 483 | return $result; 484 | } 485 | 486 | /** 487 | * Determines if node is leaf. 488 | * @return boolean whether the node is leaf. 489 | */ 490 | public function isLeaf() 491 | { 492 | $owner=$this->getOwner(); 493 | 494 | return $owner->{$this->rightAttribute}-$owner->{$this->leftAttribute}===1; 495 | } 496 | 497 | /** 498 | * Determines if node is root. 499 | * @return boolean whether the node is root. 500 | */ 501 | public function isRoot() 502 | { 503 | return $this->getOwner()->{$this->leftAttribute}==1; 504 | } 505 | 506 | /** 507 | * Returns if the current node is deleted. 508 | * @return boolean whether the node is deleted. 509 | */ 510 | public function getIsDeletedRecord() 511 | { 512 | return $this->_deleted; 513 | } 514 | 515 | /** 516 | * Sets if the current node is deleted. 517 | * @param boolean $value whether the node is deleted. 518 | */ 519 | public function setIsDeletedRecord($value) 520 | { 521 | $this->_deleted=$value; 522 | } 523 | 524 | /** 525 | * Handle 'afterConstruct' event of the owner. 526 | * @param CEvent $event event parameter. 527 | */ 528 | public function afterConstruct($event) 529 | { 530 | $owner=$this->getOwner(); 531 | self::$_cached[get_class($owner)][$this->_id=self::$_c++]=$owner; 532 | } 533 | 534 | /** 535 | * Handle 'afterFind' event of the owner. 536 | * @param CEvent $event event parameter. 537 | */ 538 | public function afterFind($event) 539 | { 540 | $owner=$this->getOwner(); 541 | self::$_cached[get_class($owner)][$this->_id=self::$_c++]=$owner; 542 | } 543 | 544 | /** 545 | * Handle 'beforeSave' event of the owner. 546 | * @param CEvent $event event parameter. 547 | * @return boolean. 548 | * @throws CDbException 549 | */ 550 | public function beforeSave($event) 551 | { 552 | if($this->_ignoreEvent) 553 | return true; 554 | else 555 | throw new CDbException(Yii::t('yiiext','You should not use CActiveRecord::save() method when NestedSetBehavior attached.')); 556 | } 557 | 558 | /** 559 | * Handle 'beforeDelete' event of the owner. 560 | * @param CEvent $event event parameter. 561 | * @return boolean. 562 | * @throws CDbException 563 | */ 564 | public function beforeDelete($event) 565 | { 566 | if($this->_ignoreEvent) 567 | return true; 568 | else 569 | throw new CDbException(Yii::t('yiiext','You should not use CActiveRecord::delete() method when NestedSetBehavior attached.')); 570 | } 571 | 572 | /** 573 | * @param int $key. 574 | * @param int $delta. 575 | */ 576 | private function shiftLeftRight($key,$delta) 577 | { 578 | $owner=$this->getOwner(); 579 | $db=$owner->getDbConnection(); 580 | 581 | foreach(array($this->leftAttribute,$this->rightAttribute) as $attribute) 582 | { 583 | $condition=$db->quoteColumnName($attribute).'>='.$key; 584 | $params=array(); 585 | 586 | if($this->hasManyRoots) 587 | { 588 | $condition.=' AND '.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount; 589 | $params[CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++]=$owner->{$this->rootAttribute}; 590 | } 591 | 592 | $owner->updateAll(array($attribute=>new CDbExpression($db->quoteColumnName($attribute).sprintf('%+d',$delta))),$condition,$params); 593 | } 594 | } 595 | 596 | /** 597 | * @param CActiveRecord $target. 598 | * @param int $key. 599 | * @param int $levelUp. 600 | * @param boolean $runValidation. 601 | * @param array $attributes. 602 | * @return boolean. 603 | * @throws CDbException 604 | * @throws CException 605 | * @throws Exception 606 | */ 607 | private function addNode($target,$key,$levelUp,$runValidation,$attributes) 608 | { 609 | $owner=$this->getOwner(); 610 | 611 | if(!$owner->getIsNewRecord()) 612 | throw new CDbException(Yii::t('yiiext','The node cannot be inserted because it is not new.')); 613 | 614 | if($this->getIsDeletedRecord()) 615 | throw new CDbException(Yii::t('yiiext','The node cannot be inserted because it is deleted.')); 616 | 617 | if($target->getIsDeletedRecord()) 618 | throw new CDbException(Yii::t('yiiext','The node cannot be inserted because target node is deleted.')); 619 | 620 | if($owner->equals($target)) 621 | throw new CException(Yii::t('yiiext','The target node should not be self.')); 622 | 623 | if(!$levelUp && $target->isRoot()) 624 | throw new CException(Yii::t('yiiext','The target node should not be root.')); 625 | 626 | if($runValidation && !$owner->validate()) 627 | return false; 628 | 629 | if($this->hasManyRoots) 630 | $owner->{$this->rootAttribute}=$target->{$this->rootAttribute}; 631 | 632 | $db=$owner->getDbConnection(); 633 | 634 | if($db->getCurrentTransaction()===null) 635 | $transaction=$db->beginTransaction(); 636 | 637 | try 638 | { 639 | $this->shiftLeftRight($key,2); 640 | $owner->{$this->leftAttribute}=$key; 641 | $owner->{$this->rightAttribute}=$key+1; 642 | $owner->{$this->levelAttribute}=$target->{$this->levelAttribute}+$levelUp; 643 | $this->_ignoreEvent=true; 644 | $result=$owner->insert($attributes); 645 | $this->_ignoreEvent=false; 646 | 647 | if(!$result) 648 | { 649 | if(isset($transaction)) 650 | $transaction->rollback(); 651 | 652 | return false; 653 | } 654 | 655 | if(isset($transaction)) 656 | $transaction->commit(); 657 | 658 | $this->correctCachedOnAddNode($key); 659 | } 660 | catch(Exception $e) 661 | { 662 | if(isset($transaction)) 663 | $transaction->rollback(); 664 | 665 | throw $e; 666 | } 667 | 668 | return true; 669 | } 670 | 671 | /** 672 | * @param array $attributes. 673 | * @return boolean. 674 | * @throws CException 675 | * @throws Exception 676 | */ 677 | private function makeRoot($attributes) 678 | { 679 | $owner=$this->getOwner(); 680 | $owner->{$this->leftAttribute}=1; 681 | $owner->{$this->rightAttribute}=2; 682 | $owner->{$this->levelAttribute}=1; 683 | 684 | if($this->hasManyRoots) 685 | { 686 | $db=$owner->getDbConnection(); 687 | 688 | if($db->getCurrentTransaction()===null) 689 | $transaction=$db->beginTransaction(); 690 | 691 | try 692 | { 693 | $this->_ignoreEvent=true; 694 | $result=$owner->insert($attributes); 695 | $this->_ignoreEvent=false; 696 | 697 | if(!$result) 698 | { 699 | if(isset($transaction)) 700 | $transaction->rollback(); 701 | 702 | return false; 703 | } 704 | 705 | $pk=$owner->{$this->rootAttribute}=$owner->getPrimaryKey(); 706 | $owner->updateByPk($pk,array($this->rootAttribute=>$pk)); 707 | 708 | if(isset($transaction)) 709 | $transaction->commit(); 710 | } 711 | catch(Exception $e) 712 | { 713 | if(isset($transaction)) 714 | $transaction->rollback(); 715 | 716 | throw $e; 717 | } 718 | } 719 | else 720 | { 721 | if($owner->roots()->exists()) 722 | throw new CException(Yii::t('yiiext','Cannot create more than one root in single root mode.')); 723 | 724 | $this->_ignoreEvent=true; 725 | $result=$owner->insert($attributes); 726 | $this->_ignoreEvent=false; 727 | 728 | if(!$result) 729 | return false; 730 | } 731 | 732 | return true; 733 | } 734 | 735 | /** 736 | * @param CActiveRecord $target. 737 | * @param int $key. 738 | * @param int $levelUp. 739 | * @return boolean. 740 | * @throws CDbException 741 | * @throws CException 742 | * @throws Exception 743 | */ 744 | private function moveNode($target,$key,$levelUp) 745 | { 746 | $owner=$this->getOwner(); 747 | 748 | if($owner->getIsNewRecord()) 749 | throw new CException(Yii::t('yiiext','The node should not be new record.')); 750 | 751 | if($this->getIsDeletedRecord()) 752 | throw new CDbException(Yii::t('yiiext','The node should not be deleted.')); 753 | 754 | if($target->getIsDeletedRecord()) 755 | throw new CDbException(Yii::t('yiiext','The target node should not be deleted.')); 756 | 757 | if($owner->equals($target)) 758 | throw new CException(Yii::t('yiiext','The target node should not be self.')); 759 | 760 | if($target->isDescendantOf($owner)) 761 | throw new CException(Yii::t('yiiext','The target node should not be descendant.')); 762 | 763 | if(!$levelUp && $target->isRoot()) 764 | throw new CException(Yii::t('yiiext','The target node should not be root.')); 765 | 766 | $db=$owner->getDbConnection(); 767 | 768 | if($db->getCurrentTransaction()===null) 769 | $transaction=$db->beginTransaction(); 770 | 771 | try 772 | { 773 | $left=$owner->{$this->leftAttribute}; 774 | $right=$owner->{$this->rightAttribute}; 775 | $levelDelta=$target->{$this->levelAttribute}-$owner->{$this->levelAttribute}+$levelUp; 776 | 777 | if($this->hasManyRoots && $owner->{$this->rootAttribute}!==$target->{$this->rootAttribute}) 778 | { 779 | foreach(array($this->leftAttribute,$this->rightAttribute) as $attribute) 780 | { 781 | $owner->updateAll(array($attribute=>new CDbExpression($db->quoteColumnName($attribute).sprintf('%+d',$right-$left+1))), 782 | $db->quoteColumnName($attribute).'>='.$key.' AND '.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount, 783 | array(CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++=>$target->{$this->rootAttribute})); 784 | } 785 | 786 | $delta=$key-$left; 787 | 788 | $owner->updateAll( 789 | array( 790 | $this->leftAttribute=>new CDbExpression($db->quoteColumnName($this->leftAttribute).sprintf('%+d',$delta)), 791 | $this->rightAttribute=>new CDbExpression($db->quoteColumnName($this->rightAttribute).sprintf('%+d',$delta)), 792 | $this->levelAttribute=>new CDbExpression($db->quoteColumnName($this->levelAttribute).sprintf('%+d',$levelDelta)), 793 | $this->rootAttribute=>$target->{$this->rootAttribute}, 794 | ), 795 | $db->quoteColumnName($this->leftAttribute).'>='.$left.' AND '. 796 | $db->quoteColumnName($this->rightAttribute).'<='.$right.' AND '. 797 | $db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount, 798 | array(CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++=>$owner->{$this->rootAttribute})); 799 | 800 | $this->shiftLeftRight($right+1,$left-$right-1); 801 | 802 | if(isset($transaction)) 803 | $transaction->commit(); 804 | 805 | $this->correctCachedOnMoveBetweenTrees($key,$levelDelta,$target->{$this->rootAttribute}); 806 | } 807 | else 808 | { 809 | $delta=$right-$left+1; 810 | $this->shiftLeftRight($key,$delta); 811 | 812 | if($left>=$key) 813 | { 814 | $left+=$delta; 815 | $right+=$delta; 816 | } 817 | 818 | $condition=$db->quoteColumnName($this->leftAttribute).'>='.$left.' AND '.$db->quoteColumnName($this->rightAttribute).'<='.$right; 819 | $params=array(); 820 | 821 | if($this->hasManyRoots) 822 | { 823 | $condition.=' AND '.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount; 824 | $params[CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++]=$owner->{$this->rootAttribute}; 825 | } 826 | 827 | $owner->updateAll(array($this->levelAttribute=>new CDbExpression($db->quoteColumnName($this->levelAttribute).sprintf('%+d',$levelDelta))),$condition,$params); 828 | 829 | foreach(array($this->leftAttribute,$this->rightAttribute) as $attribute) 830 | { 831 | $condition=$db->quoteColumnName($attribute).'>='.$left.' AND '.$db->quoteColumnName($attribute).'<='.$right; 832 | $params=array(); 833 | 834 | if($this->hasManyRoots) 835 | { 836 | $condition.=' AND '.$db->quoteColumnName($this->rootAttribute).'='.CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount; 837 | $params[CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount++]=$owner->{$this->rootAttribute}; 838 | } 839 | 840 | $owner->updateAll(array($attribute=>new CDbExpression($db->quoteColumnName($attribute).sprintf('%+d',$key-$left))),$condition,$params); 841 | } 842 | 843 | $this->shiftLeftRight($right+1,-$delta); 844 | 845 | if(isset($transaction)) 846 | $transaction->commit(); 847 | 848 | $this->correctCachedOnMoveNode($key,$levelDelta); 849 | } 850 | } 851 | catch(Exception $e) 852 | { 853 | if(isset($transaction)) 854 | $transaction->rollback(); 855 | 856 | throw $e; 857 | } 858 | 859 | return true; 860 | } 861 | 862 | /** 863 | * Correct cache for {@link NestedSetBehavior::delete()} and {@link NestedSetBehavior::deleteNode()}. 864 | */ 865 | private function correctCachedOnDelete() 866 | { 867 | $owner=$this->getOwner(); 868 | $left=$owner->{$this->leftAttribute}; 869 | $right=$owner->{$this->rightAttribute}; 870 | $key=$right+1; 871 | $delta=$left-$right-1; 872 | 873 | foreach(self::$_cached[get_class($owner)] as $node) 874 | { 875 | if($node->getIsNewRecord() || $node->getIsDeletedRecord()) 876 | continue; 877 | 878 | if($this->hasManyRoots && $owner->{$this->rootAttribute}!==$node->{$this->rootAttribute}) 879 | continue; 880 | 881 | if($node->{$this->leftAttribute}>=$left && $node->{$this->rightAttribute}<=$right) 882 | $node->setIsDeletedRecord(true); 883 | else 884 | { 885 | if($node->{$this->leftAttribute}>=$key) 886 | $node->{$this->leftAttribute}+=$delta; 887 | 888 | if($node->{$this->rightAttribute}>=$key) 889 | $node->{$this->rightAttribute}+=$delta; 890 | } 891 | } 892 | } 893 | 894 | /** 895 | * Correct cache for {@link NestedSetBehavior::addNode()}. 896 | * @param int $key. 897 | */ 898 | private function correctCachedOnAddNode($key) 899 | { 900 | $owner=$this->getOwner(); 901 | 902 | foreach(self::$_cached[get_class($owner)] as $node) 903 | { 904 | if($node->getIsNewRecord() || $node->getIsDeletedRecord()) 905 | continue; 906 | 907 | if($this->hasManyRoots && $owner->{$this->rootAttribute}!==$node->{$this->rootAttribute}) 908 | continue; 909 | 910 | if($owner===$node) 911 | continue; 912 | 913 | if($node->{$this->leftAttribute}>=$key) 914 | $node->{$this->leftAttribute}+=2; 915 | 916 | if($node->{$this->rightAttribute}>=$key) 917 | $node->{$this->rightAttribute}+=2; 918 | } 919 | } 920 | 921 | /** 922 | * Correct cache for {@link NestedSetBehavior::moveNode()}. 923 | * @param int $key. 924 | * @param int $levelDelta. 925 | */ 926 | private function correctCachedOnMoveNode($key,$levelDelta) 927 | { 928 | $owner=$this->getOwner(); 929 | $left=$owner->{$this->leftAttribute}; 930 | $right=$owner->{$this->rightAttribute}; 931 | $delta=$right-$left+1; 932 | 933 | if($left>=$key) 934 | { 935 | $left+=$delta; 936 | $right+=$delta; 937 | } 938 | 939 | $delta2=$key-$left; 940 | 941 | foreach(self::$_cached[get_class($owner)] as $node) 942 | { 943 | if($node->getIsNewRecord() || $node->getIsDeletedRecord()) 944 | continue; 945 | 946 | if($this->hasManyRoots && $owner->{$this->rootAttribute}!==$node->{$this->rootAttribute}) 947 | continue; 948 | 949 | if($node->{$this->leftAttribute}>=$key) 950 | $node->{$this->leftAttribute}+=$delta; 951 | 952 | if($node->{$this->rightAttribute}>=$key) 953 | $node->{$this->rightAttribute}+=$delta; 954 | 955 | if($node->{$this->leftAttribute}>=$left && $node->{$this->rightAttribute}<=$right) 956 | $node->{$this->levelAttribute}+=$levelDelta; 957 | 958 | if($node->{$this->leftAttribute}>=$left && $node->{$this->leftAttribute}<=$right) 959 | $node->{$this->leftAttribute}+=$delta2; 960 | 961 | if($node->{$this->rightAttribute}>=$left && $node->{$this->rightAttribute}<=$right) 962 | $node->{$this->rightAttribute}+=$delta2; 963 | 964 | if($node->{$this->leftAttribute}>=$right+1) 965 | $node->{$this->leftAttribute}-=$delta; 966 | 967 | if($node->{$this->rightAttribute}>=$right+1) 968 | $node->{$this->rightAttribute}-=$delta; 969 | } 970 | } 971 | 972 | /** 973 | * Correct cache for {@link NestedSetBehavior::moveNode()}. 974 | * @param int $key. 975 | * @param int $levelDelta. 976 | * @param int $root. 977 | */ 978 | private function correctCachedOnMoveBetweenTrees($key,$levelDelta,$root) 979 | { 980 | $owner=$this->getOwner(); 981 | $left=$owner->{$this->leftAttribute}; 982 | $right=$owner->{$this->rightAttribute}; 983 | $delta=$right-$left+1; 984 | $delta2=$key-$left; 985 | $delta3=$left-$right-1; 986 | 987 | foreach(self::$_cached[get_class($owner)] as $node) 988 | { 989 | if($node->getIsNewRecord() || $node->getIsDeletedRecord()) 990 | continue; 991 | 992 | if($node->{$this->rootAttribute}===$root) 993 | { 994 | if($node->{$this->leftAttribute}>=$key) 995 | $node->{$this->leftAttribute}+=$delta; 996 | 997 | if($node->{$this->rightAttribute}>=$key) 998 | $node->{$this->rightAttribute}+=$delta; 999 | } 1000 | else if($node->{$this->rootAttribute}===$owner->{$this->rootAttribute}) 1001 | { 1002 | if($node->{$this->leftAttribute}>=$left && $node->{$this->rightAttribute}<=$right) 1003 | { 1004 | $node->{$this->leftAttribute}+=$delta2; 1005 | $node->{$this->rightAttribute}+=$delta2; 1006 | $node->{$this->levelAttribute}+=$levelDelta; 1007 | $node->{$this->rootAttribute}=$root; 1008 | } 1009 | else 1010 | { 1011 | if($node->{$this->leftAttribute}>=$right+1) 1012 | $node->{$this->leftAttribute}+=$delta3; 1013 | 1014 | if($node->{$this->rightAttribute}>=$right+1) 1015 | $node->{$this->rightAttribute}+=$delta3; 1016 | } 1017 | } 1018 | } 1019 | } 1020 | 1021 | /** 1022 | * Destructor. 1023 | */ 1024 | public function __destruct() 1025 | { 1026 | unset(self::$_cached[get_class($this->getOwner())][$this->_id]); 1027 | } 1028 | } --------------------------------------------------------------------------------