From 7f8633ea9805f16dde9421ff4582431d738623a5 Mon Sep 17 00:00:00 2001 From: PaulZi Date: Fri, 24 Jul 2015 00:46:14 +0300 Subject: [PATCH] First commit --- .gitignore | 15 + LICENSE | 22 + NestedSetsBehavior.php | 648 +++++++++++++++++++++ NestedSetsQueryTrait.php | 30 + README.md | 233 ++++++++ composer.json | 31 + sample-migrations/m150722_150000_single_tree.php | 25 + sample-migrations/m150722_150100_multiple_tree.php | 26 + 8 files changed, 1030 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NestedSetsBehavior.php create mode 100644 NestedSetsQueryTrait.php create mode 100644 README.md create mode 100644 composer.json create mode 100644 sample-migrations/m150722_150000_single_tree.php create mode 100644 sample-migrations/m150722_150100_multiple_tree.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c157c13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# phpstorm project files +.idea + +# windows thumbnail cache +Thumbs.db + +# composer vendor dir +/vendor + +# composer itself is not needed +composer.phar +composer.lock + +# Mac DS_Store Files +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..360f12a --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 PaulZi (pavel.zimakoff@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/NestedSetsBehavior.php b/NestedSetsBehavior.php new file mode 100644 index 0000000..da764a4 --- /dev/null +++ b/NestedSetsBehavior.php @@ -0,0 +1,648 @@ + + * @license MIT (https://github.com/paulzi/yii2-nested-sets/blob/master/LICENSE) + */ + +namespace paulzi\nestedsets; + +use Yii; +use yii\base\Behavior; +use yii\base\Exception; +use yii\base\NotSupportedException; +use yii\db\ActiveRecord; +use yii\db\Expression; + +/** + * Nested Sets Behavior for Yii2 + * @author PaulZi + * @author Alexander Kochetov + * + * @property ActiveRecord $owner + */ +class NestedSetsBehavior extends Behavior +{ + const OPERATION_MAKE_ROOT = 1; + const OPERATION_PREPEND_TO = 2; + const OPERATION_APPEND_TO = 3; + const OPERATION_INSERT_BEFORE = 4; + const OPERATION_INSERT_AFTER = 5; + const OPERATION_DELETE_ALL = 6; + + + /** + * @var string|null + */ + public $treeAttribute; + + /** + * @var string + */ + public $leftAttribute = 'lft'; + + /** + * @var string + */ + public $rightAttribute = 'rgt'; + + /** + * @var string + */ + public $depthAttribute = 'depth'; + + /** + * @var string|null + */ + protected $operation; + + /** + * @var ActiveRecord|self|null + */ + protected $node; + + + /** + * @inheritdoc + */ + public function events() + { + return [ + ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert', + ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert', + ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate', + ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate', + ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete', + ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete', + ]; + } + + /** + * @param int|null $depth + * @return \yii\db\ActiveQuery + */ + public function getParents($depth = null) + { + $tableName = $this->owner->tableName(); + $condition = [ + 'and', + ['<', "{$tableName}.[[{$this->leftAttribute}]]", $this->owner->getAttribute($this->leftAttribute)], + ['>', "{$tableName}.[[{$this->rightAttribute}]]", $this->owner->getAttribute($this->rightAttribute)], + ]; + if ($depth !== null) { + $condition[] = ['>=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->getAttribute($this->depthAttribute) - $depth]; + } + + $query = $this->owner->find() + ->andWhere($condition) + ->andWhere($this->treeCondition()) + ->addOrderBy(["{$tableName}.[[{$this->leftAttribute}]]" => SORT_ASC]); + $query->multiple = true; + + return $query; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getParent() + { + $tableName = $this->owner->tableName(); + $query = $this->getParents(1) + ->orderBy(["{$tableName}.[[{$this->leftAttribute}]]" => SORT_DESC]) + ->limit(1); + $query->multiple = false; + return $query; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getRoot() + { + $tableName = $this->owner->tableName(); + $query = $this->owner->find() + ->andWhere(["{$tableName}.[[{$this->leftAttribute}]]" => 1]) + ->andWhere($this->treeCondition()) + ->limit(1); + $query->multiple = false; + return $query; + } + + /** + * @param int|null $depth + * @param bool $andSelf + * @param bool $backOrder + * @return \yii\db\ActiveQuery + */ + public function getDescendants($depth = null, $andSelf = false, $backOrder = false) + { + $tableName = $this->owner->tableName(); + $attribute = $backOrder ? $this->rightAttribute : $this->leftAttribute; + $condition = [ + 'and', + [$andSelf ? '>=' : '>', "{$tableName}.[[{$attribute}]]", $this->owner->getAttribute($this->leftAttribute)], + [$andSelf ? '<=' : '<', "{$tableName}.[[{$attribute}]]", $this->owner->getAttribute($this->rightAttribute)], + ]; + + if ($depth !== null) { + $condition[] = ['<=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->getAttribute($this->depthAttribute) + $depth]; + } + + $query = $this->owner->find() + ->andWhere($condition) + ->andWhere($this->treeCondition()) + ->addOrderBy(["{$tableName}.[[{$attribute}]]" => $backOrder ? SORT_DESC : SORT_ASC]); + $query->multiple = true; + + return $query; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getChildren() + { + return $this->getDescendants(1); + } + + /** + * @param int|null $depth + * @return \yii\db\ActiveQuery + */ + public function getLeaves($depth = null) + { + $tableName = $this->owner->tableName(); + $query = $this->getDescendants($depth) + ->andWhere(["{$tableName}.[[{$this->leftAttribute}]]" => new Expression("{$tableName}.[[{$this->rightAttribute}]] - 1")]); + $query->multiple = true; + return $query; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getPrev() + { + $tableName = $this->owner->tableName(); + $query = $this->owner->find() + ->andWhere(["{$tableName}.[[{$this->rightAttribute}]]" => $this->owner->getAttribute($this->leftAttribute) - 1]) + ->andWhere($this->treeCondition()) + ->limit(1); + $query->multiple = false; + return $query; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getNext() + { + $tableName = $this->owner->tableName(); + $query = $this->owner->find() + ->andWhere(["{$tableName}.[[{$this->leftAttribute}]]" => $this->owner->getAttribute($this->rightAttribute) + 1]) + ->andWhere($this->treeCondition()) + ->limit(1); + $query->multiple = false; + return $query; + } + + /** + * @return bool + */ + public function isRoot() + { + return $this->owner->getAttribute($this->leftAttribute) === 1; + } + + /** + * @param ActiveRecord $node + * @return bool + */ + public function isChildOf($node) + { + $result = $this->owner->getAttribute($this->leftAttribute) > $node->getAttribute($this->leftAttribute) + && $this->owner->getAttribute($this->rightAttribute) < $node->getAttribute($this->rightAttribute); + + if ($result && $this->treeAttribute !== null) { + $result = $this->owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute); + } + + return $result; + } + + /** + * @return bool + */ + public function isLeaf() + { + return $this->owner->getAttribute($this->rightAttribute) - $this->owner->getAttribute($this->leftAttribute) === 1; + } + + /** + * @return ActiveRecord + */ + public function makeRoot() + { + $this->operation = self::OPERATION_MAKE_ROOT; + return $this->owner; + } + + /** + * @param ActiveRecord $node + * @return ActiveRecord + */ + public function prependTo($node) + { + $this->operation = self::OPERATION_PREPEND_TO; + $this->node = $node; + return $this->owner; + } + + /** + * @param ActiveRecord $node + * @return ActiveRecord + */ + public function appendTo($node) + { + $this->operation = self::OPERATION_APPEND_TO; + $this->node = $node; + return $this->owner; + } + + /** + * @param ActiveRecord $node + * @return ActiveRecord + */ + public function insertBefore($node) + { + $this->operation = self::OPERATION_INSERT_BEFORE; + $this->node = $node; + return $this->owner; + } + + /** + * @param ActiveRecord $node + * @return ActiveRecord + */ + public function insertAfter($node) + { + $this->operation = self::OPERATION_INSERT_AFTER; + $this->node = $node; + return $this->owner; + } + + /** + * @return bool|int + * @throws \Exception + * @throws \yii\db\Exception + */ + public function deleteWithChildren() + { + $this->operation = self::OPERATION_DELETE_ALL; + if (!$this->owner->isTransactional(ActiveRecord::OP_DELETE)) { + $transaction = $this->owner->getDb()->beginTransaction(); + try { + $result = $this->deleteWithChildrenInternal(); + if ($result === false) { + $transaction->rollBack(); + } else { + $transaction->commit(); + } + return $result; + } catch (\Exception $e) { + $transaction->rollBack(); + throw $e; + } + } else { + $result = $this->deleteWithChildrenInternal(); + } + return $result; + } + + /** + * @throws Exception + * @throws NotSupportedException + */ + public function beforeInsert() + { + if ($this->node !== null && !$this->node->getIsNewRecord()) { + $this->node->refresh(); + } + switch ($this->operation) { + case self::OPERATION_MAKE_ROOT: + $condition = array_merge([$this->leftAttribute => 1], $this->treeCondition()); + if ($this->owner->findOne($condition) !== null) { + throw new Exception('Can not create more than one root.'); + } + $this->owner->setAttribute($this->leftAttribute, 1); + $this->owner->setAttribute($this->rightAttribute, 2); + $this->owner->setAttribute($this->depthAttribute, 0); + break; + + case self::OPERATION_PREPEND_TO: + $this->insertNode($this->node->getAttribute($this->leftAttribute) + 1, 1); + break; + + case self::OPERATION_APPEND_TO: + $this->insertNode($this->node->getAttribute($this->rightAttribute), 1); + break; + + case self::OPERATION_INSERT_BEFORE: + $this->insertNode($this->node->getAttribute($this->leftAttribute), 0); + break; + + case self::OPERATION_INSERT_AFTER: + $this->insertNode($this->node->getAttribute($this->rightAttribute) + 1, 0); + break; + + default: + throw new NotSupportedException('Method "'. $this->owner->className() . '::insert" is not supported for inserting new nodes.'); + } + } + + /** + * @throws Exception + */ + public function afterInsert() + { + if ($this->operation === self::OPERATION_MAKE_ROOT && $this->treeAttribute !== null && $this->owner->getAttribute($this->treeAttribute) === null) { + $id = $this->owner->getPrimaryKey(); + $this->owner->setAttribute($this->treeAttribute, $id); + + $primaryKey = $this->owner->primaryKey(); + if (!isset($primaryKey[0])) { + throw new Exception('"' . $this->owner->className() . '" must have a primary key.'); + } + + $this->owner->updateAll([$this->treeAttribute => $id], [$primaryKey[0] => $id]); + } + $this->operation = null; + $this->node = null; + } + + /** + * @throws Exception + */ + public function beforeUpdate() + { + if ($this->node !== null && !$this->node->getIsNewRecord()) { + $this->node->refresh(); + } + + switch ($this->operation) { + case self::OPERATION_MAKE_ROOT: + if ($this->treeAttribute === null) { + throw new Exception('Can not move a node as the root when "treeAttribute" is not set.'); + } + if ($this->isRoot()) { + throw new Exception('Can not move the root node as the root.'); + } + break; + + case self::OPERATION_INSERT_BEFORE: + case self::OPERATION_INSERT_AFTER: + if ($this->node->isRoot()) { + throw new Exception('Can not move a node before/after root.'); + } + + case self::OPERATION_PREPEND_TO: + case self::OPERATION_APPEND_TO: + if ($this->node->getIsNewRecord()) { + throw new Exception('Can not move a node when the target node is new record.'); + } + + if ($this->owner->equals($this->node)) { + throw new Exception('Can not move a node when the target node is same.'); + } + + if ($this->node->isChildOf($this->owner)) { + throw new Exception('Can not move a node when the target node is child.'); + } + } + } + + /** + * + */ + public function afterUpdate() + { + switch ($this->operation) { + case self::OPERATION_MAKE_ROOT: + $this->moveNodeAsRoot(); + break; + + case self::OPERATION_PREPEND_TO: + $this->moveNode($this->node->getAttribute($this->leftAttribute) + 1, 1); + break; + + case self::OPERATION_APPEND_TO: + $this->moveNode($this->node->getAttribute($this->rightAttribute), 1); + break; + + case self::OPERATION_INSERT_BEFORE: + $this->moveNode($this->node->getAttribute($this->leftAttribute), 0); + break; + + case self::OPERATION_INSERT_AFTER: + $this->moveNode($this->node->getAttribute($this->rightAttribute) + 1, 0); + break; + } + $this->operation = null; + $this->node = null; + } + + /** + * @throws Exception + */ + public function beforeDelete() + { + if ($this->owner->getIsNewRecord()) { + throw new Exception('Can not delete a node when it is new record.'); + } + if ($this->isRoot() && $this->operation !== self::OPERATION_DELETE_ALL) { + throw new Exception('Method "'. $this->owner->className() . '::delete" is not supported for deleting root nodes.'); + } + $this->owner->refresh(); + } + + /** + * + */ + public function afterDelete() + { + $left = $this->owner->getAttribute($this->leftAttribute); + $right = $this->owner->getAttribute($this->rightAttribute); + if ($this->operation === static::OPERATION_DELETE_ALL || $this->isLeaf()) { + $this->shift($right + 1, null, $left - $right - 1); + } else { + $this->owner->updateAll( + [ + $this->leftAttribute => new Expression("[[{$this->leftAttribute}]] - 1"), + $this->rightAttribute => new Expression("[[{$this->rightAttribute}]] - 1"), + $this->depthAttribute => new Expression("[[{$this->depthAttribute}]] - 1"), + ], + $this->getDescendants()->where + ); + $this->shift($right + 1, null, -2); + } + $this->operation = null; + $this->node = null; + } + + /** + * @return int + */ + protected function deleteWithChildrenInternal() + { + if (!$this->owner->beforeDelete()) { + return false; + } + $result = $this->owner->deleteAll($this->getDescendants(null, true)->where); + $this->owner->setOldAttributes(null); + $this->owner->afterDelete(); + return $result; + } + + /** + * @param int $to + * @param int $depth + * @throws Exception + */ + protected function insertNode($to, $depth = 0) + { + if ($this->node->getIsNewRecord()) { + throw new Exception('Can not create a node when the target node is new record.'); + } + + if ($depth === 0 && $this->node->isRoot()) { + throw new Exception('Can not insert a node before/after root.'); + } + $this->owner->setAttribute($this->leftAttribute, $to); + $this->owner->setAttribute($this->rightAttribute, $to + 1); + $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute) + $depth); + if ($this->treeAttribute !== null) { + $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute)); + } + $this->shift($to, null, 2); + } + + /** + * @param int $to + * @param int $depth + * @throws Exception + */ + protected function moveNode($to, $depth = 0) + { + $left = $this->owner->getAttribute($this->leftAttribute); + $right = $this->owner->getAttribute($this->rightAttribute); + $depth = $this->owner->getAttribute($this->depthAttribute) - $this->node->getAttribute($this->depthAttribute) - $depth; + if ($this->treeAttribute === null || $this->owner->getAttribute($this->treeAttribute) === $this->node->getAttribute($this->treeAttribute)) { + // same root + $this->owner->updateAll( + [$this->depthAttribute => new Expression("-[[{$this->depthAttribute}]]" . sprintf('%+d', $depth))], + $this->getDescendants(null, true)->where + ); + $delta = $right - $left + 1; + if ($left >= $to) { + $this->shift($to, $left - 1, $delta); + $delta = $to - $left; + } else { + $this->shift($right + 1, $to, -$delta); + $delta = $to - $right; + } + $this->owner->updateAll( + [ + $this->leftAttribute => new Expression("[[{$this->leftAttribute}]]" . sprintf('%+d', $delta)), + $this->rightAttribute => new Expression("[[{$this->rightAttribute}]]" . sprintf('%+d', $delta)), + $this->depthAttribute => new Expression("-[[{$this->depthAttribute}]]"), + ], + [ + 'and', + $this->getDescendants(null, true)->where, + ['<', $this->depthAttribute, 0], + ] + ); + } else { + // move from other root + $tree = $this->node->getAttribute($this->treeAttribute); + $this->shift($to, null, $right - $left + 1, $tree); + $delta = $to - $left; + $this->owner->updateAll( + [ + $this->leftAttribute => new Expression("[[{$this->leftAttribute}]]" . sprintf('%+d', $delta)), + $this->rightAttribute => new Expression("[[{$this->rightAttribute}]]" . sprintf('%+d', $delta)), + $this->depthAttribute => new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', -$depth)), + $this->treeAttribute => $tree, + ], + $this->getDescendants(null, true)->where + ); + $this->shift($right + 1, null, $left - $right - 1); + } + } + + /** + * + */ + protected function moveNodeAsRoot() + { + $left = $this->owner->getAttribute($this->leftAttribute); + $right = $this->owner->getAttribute($this->rightAttribute); + $depth = $this->owner->getAttribute($this->depthAttribute); + $tree = $this->owner->getPrimaryKey(); + if ($this->owner->getOldAttribute($this->treeAttribute) !== $this->owner->getAttribute($this->treeAttribute)) { + $tree = $this->owner->getAttribute($this->treeAttribute); + } + + $this->owner->updateAll( + [ + $this->leftAttribute => new Expression("[[{$this->leftAttribute}]]" . sprintf('%+d', 1 - $left)), + $this->rightAttribute => new Expression("[[{$this->rightAttribute}]]" . sprintf('%+d', 1 - $left)), + $this->depthAttribute => new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', -$depth)), + $this->treeAttribute => $tree, + ], + $this->getDescendants(null, true)->where + ); + $this->shift($right + 1, null, $left - $right - 1); + } + + + + /** + * @param int $from + * @param int $to + * @param int $delta + * @param int|null $tree + */ + protected function shift($from, $to, $delta, $tree = null) + { + if ($delta !== 0 && ($to === null || $to >= $from)) { + if ($tree === null) { + $tree = $this->owner->getAttribute($this->treeAttribute); + } + foreach ([$this->leftAttribute, $this->rightAttribute] as $i => $attribute) { + $this->owner->updateAll( + [$attribute => new Expression("[[{$attribute}]]" . sprintf('%+d', $delta))], + [ + 'and', + $to === null ? ['>=', $attribute, $from] : ['between', $attribute, $from, $to], + [$this->treeAttribute => $tree], + ] + ); + } + } + } + + /** + * @return array + */ + protected function treeCondition() + { + $tableName = $this->owner->tableName(); + if ($this->treeAttribute === null) { + return []; + } else { + return ["{$tableName}.[[{$this->treeAttribute}]]" => $this->owner->getAttribute($this->treeAttribute)]; + } + } +} diff --git a/NestedSetsQueryTrait.php b/NestedSetsQueryTrait.php new file mode 100644 index 0000000..792f0ae --- /dev/null +++ b/NestedSetsQueryTrait.php @@ -0,0 +1,30 @@ + + * @license MIT (https://github.com/paulzi/yii2-nested-sets/blob/master/LICENSE) + */ + +namespace paulzi\nestedsets; + +/** + * @author PaulZi + */ +trait NestedSetsQueryTrait +{ + /** + * @return \yii\db\ActiveQuery + */ + public function roots() + { + /** @var \yii\db\ActiveQuery $this */ + $class = $this->modelClass; + if (isset($class::$nestedSetsLeftAttribute)) { + return $this->andWhere([$class::$nestedSetsLeftAttribute => 1]); + } else { + /** @var \yii\db\ActiveRecord|NestedSetsBehavior $model */ + $model = new $class; + return $this->andWhere([$model->leftAttribute => 1]); + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..0593e3b --- /dev/null +++ b/README.md @@ -0,0 +1,233 @@ +# Yii2 Nested Sets Behavior + +Implementation of nested sets algorithm for storing the trees in DB tables. + +## Install + +Install via Composer: + +```bash +composer require paulzi/yii2-nested-sets +``` + +or add + +```bash +"paulzi/yii2-nested-sets" : "^1.0" +``` + +to the `require` section of your `composer.json` file. + +## Migrations + +Sample migrations are in the folder `sample-migrations`: + +- `m150722_150000_single_tree.php` - for single tree tables; +- `m150722_150100_multiple_tree.php` - for multiple tree tables. + +## Configuring + +```php +use paulzi\nestedsets\NestedSetsBehavior; + +class Sample extends \yii\db\ActiveRecord +{ + public function behaviors() { + return [ + [ + 'class' => NestedSetsBehavior::className(), + // 'treeAttribute' => 'tree', + ], + ]; + } + + public function transactions() + { + return [ + self::SCENARIO_DEFAULT => self::OP_ALL, + ]; + } +} +``` + +Optional you can setup Query for finding roots: + +```php +class Sample extends \yii\db\ActiveRecord +{ + public static function find() + { + return new SampleQuery(get_called_class()); + } +} +``` + +Query class: + +```php +use paulzi\nestedsets\NestedSetsQueryTrait; + +class SampleQuery extends \yii\db\ActiveQuery +{ + use NestedSetsQueryTrait; +} +``` + +## Options + +- `$treeAttribute = null` - setup tree attribute for multiple tree in table schema. +- `$leftAttribute = 'lft'` - left attribute in table schema. +- `$rightAttribute = 'rgt'` - right attribute in table schema. +- `$depthAttribute = 'depth'` - depth attribute in table schema (note: it must be signed int). + +## Usage + +### Selection + +**Getting the root nodes** + +If you connect `NestedSetsQueryTrait`, you can get all the root nodes: + +```php +$roots = Sample::find()->roots()->all(); +``` + +**Getting ancestors of a node** + +To get ancestors of a node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$parents = $node11->parents; // via relation +$parents = $node11->getParents()->all(); // via query +$parents = $node11->getParents(2)->all(); // get 2 levels of ancestors +``` + +To get parent of a node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$parent = $node11->parent; // via relation +$parent = $node11->getParent()->one(); // via query +``` + +To get root of a node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$root = $node11->root; // via relation +$root = $node11->getRoot()->one(); // via query +``` + +**Getting descendants of a node** + +To get all the descendants of a node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$descendants = $node11->descendants; // via relation +$descendants = $node11->getDescendants()->all(); // via query +$descendants = $node11->getDescendants(2, true)->all(); // get 2 levels of descendants and self node +$descendants = $node11->getDescendants(3, false, true)->all(); // get 3 levels of descendants in back order +``` + +To get the children of a node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$children = $node11->children; // via relation +$children = $node11->getChildren()->all(); // via query +``` + +**Getting the leaves nodes** + +To get all the leaves of a node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$leaves = $node11->leaves; // via relation +$leaves = $node11->getLeaves(2)->all(); // get 2 levels of leaves via query +``` + +**Getting the neighbors nodes** + +To get the next node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$next = $node11->next; // via relation +$next = $node11->getNext()->one(); // via query +``` + +To get the previous node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$prev = $node11->prev; // via relation +$prev = $node11->getPrev()->one(); // via query +``` + +### Some checks + +```php +$node1 = Sample::findOne(['name' => 'node 1']); +$node11 = Sample::findOne(['name' => 'node 1.1']); +$node11->isRoot() - return true, if node is root +$node11->isLeaf() - return true, if node is leaf +$node11->isChildOf($node1) - return true, if node11 is child of $node1 +``` + + +### Modifications + +To make a root node: + +```php +$node11 = new Sample(); +$node11->name = 'node 1.1'; +$node11->makeRoot()->save(); +``` + +*Note: if you allow multiple trees and attribute `tree` is not set, it automatically takes the primary key value.* + +To prepend a node as the first child of another node: + +```php +$node1 = Sample::findOne(['name' => 'node 1']); +$node11 = new Sample(); +$node11->name = 'node 1.1'; +$node11->prependTo($node1)->save(); // inserting new node +``` + +To append a node as the last child of another node: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$node12 = Sample::findOne(['name' => 'node 1.2']); +$node12->appendTo($node11)->save(); // move existing node +``` + +To insert a node before another node: + +```php +$node13 = Sample::findOne(['name' => 'node 1.3']); +$node12 = new Sample(); +$node12->name = 'node 1.2'; +$node12->insertBefore($node13)->save(); // inserting new node +``` + +To insert a node after another node: + +```php +$node13 = Sample::findOne(['name' => 'node 1.3']); +$node14 = Sample::findOne(['name' => 'node 1.4']); +$node14->insertAfter($node13)->save(); // move existing node +``` + +To delete a node with descendants: + +```php +$node11 = Sample::findOne(['name' => 'node 1.1']); +$node11->delete(); // delete node, children come up to the parent +$node11->deleteWithChildren(); // delete node and all descendants +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e4573d7 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "paulzi/yii2-nested-sets", + "description": "Nested Sets Behavior for Yii2", + "keywords": ["yii2", "nested sets"], + "type": "yii2-extension", + "license": "MIT", + "support": { + "issues": "https://github.com/paulzi/yii2-nested-sets/issues?state=open", + "source": "https://github.com/paulzi/yii2-nested-sets" + }, + "authors": [ + { + "name": "PaulZi", + "email": "pavel.zimakoff@gmail.com" + }, + { + "name": "Alexander Kochetov", + "email": "creocoder@gmail.com" + } + ], + "minimum-stability": "stable", + "require": { + "php": ">=5.4.0", + "yiisoft/yii2": "~2.0.0" + }, + "autoload": { + "psr-4": { + "paulzi\\nestedsets\\": "" + } + } +} \ No newline at end of file diff --git a/sample-migrations/m150722_150000_single_tree.php b/sample-migrations/m150722_150000_single_tree.php new file mode 100644 index 0000000..55157b1 --- /dev/null +++ b/sample-migrations/m150722_150000_single_tree.php @@ -0,0 +1,25 @@ +db->driverName === 'mysql') { + // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + } + + $this->createTable('{{%single_tree}}', [ + 'id' => Schema::TYPE_PK, + 'lft' => Schema::TYPE_INTEGER . ' NOT NULL', + 'rgt' => Schema::TYPE_INTEGER . ' NOT NULL', + 'depth' => Schema::TYPE_INTEGER . ' NOT NULL', + ], $tableOptions); + $this->createIndex('lft', '{{%single_tree}}', ['lft', 'rgt']); + $this->createIndex('rgt', '{{%single_tree}}', ['rgt']); + } +} diff --git a/sample-migrations/m150722_150100_multiple_tree.php b/sample-migrations/m150722_150100_multiple_tree.php new file mode 100644 index 0000000..0e82b61 --- /dev/null +++ b/sample-migrations/m150722_150100_multiple_tree.php @@ -0,0 +1,26 @@ +db->driverName === 'mysql') { + // http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + } + + $this->createTable('{{%multiple_tree}}', [ + 'id' => Schema::TYPE_PK, + 'tree' => Schema::TYPE_INTEGER . ' NULL', + 'lft' => Schema::TYPE_INTEGER . ' NOT NULL', + 'rgt' => Schema::TYPE_INTEGER . ' NOT NULL', + 'depth' => Schema::TYPE_INTEGER . ' NOT NULL', + ], $tableOptions); + $this->createIndex('lft', '{{%multiple_tree}}', ['tree', 'lft', 'rgt']); + $this->createIndex('rgt', '{{%multiple_tree}}', ['tree', 'rgt']); + } +}