Browse Source

First commit

tags/v1.0.0 v1.0.0
PaulZi 9 years ago
commit
7f8633ea98
  1. 15
      .gitignore
  2. 22
      LICENSE
  3. 648
      NestedSetsBehavior.php
  4. 30
      NestedSetsQueryTrait.php
  5. 233
      README.md
  6. 31
      composer.json
  7. 25
      sample-migrations/m150722_150000_single_tree.php
  8. 26
      sample-migrations/m150722_150100_multiple_tree.php

15
.gitignore vendored

@ -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

22
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.

648
NestedSetsBehavior.php

@ -0,0 +1,648 @@
<?php
/**
* @link https://github.com/paulzi/yii2-nested-sets
* @copyright Copyright (c) 2015 PaulZi <pavel.zimakoff@gmail.com>
* @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 <pavel.zimakoff@gmail.com>
* @author Alexander Kochetov <https://github.com/creocoder>
*
* @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)];
}
}
}

30
NestedSetsQueryTrait.php

@ -0,0 +1,30 @@
<?php
/**
* @link https://github.com/paulzi/yii2-nested-sets
* @copyright Copyright (c) 2015 PaulZi <pavel.zimakoff@gmail.com>
* @license MIT (https://github.com/paulzi/yii2-nested-sets/blob/master/LICENSE)
*/
namespace paulzi\nestedsets;
/**
* @author PaulZi <pavel.zimakoff@gmail.com>
*/
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]);
}
}
}

233
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
```

31
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\\": ""
}
}
}

25
sample-migrations/m150722_150000_single_tree.php

@ -0,0 +1,25 @@
<?php
use yii\db\Schema;
use yii\db\Migration;
class m150722_150000_single_tree extends Migration
{
public function up()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
}
$this->createTable('{{%single_tree}}', [
'id' => Schema::TYPE_PK,
'lft' => Schema::TYPE_INTEGER . ' NOT NULL',
'rgt' => Schema::TYPE_INTEGER . ' NOT NULL',
'depth' => Schema::TYPE_INTEGER . ' NOT NULL',
], $tableOptions);
$this->createIndex('lft', '{{%single_tree}}', ['lft', 'rgt']);
$this->createIndex('rgt', '{{%single_tree}}', ['rgt']);
}
}

26
sample-migrations/m150722_150100_multiple_tree.php

@ -0,0 +1,26 @@
<?php
use yii\db\Schema;
use yii\db\Migration;
class m150722_150100_multiple_tree extends Migration
{
public function up()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
}
$this->createTable('{{%multiple_tree}}', [
'id' => Schema::TYPE_PK,
'tree' => Schema::TYPE_INTEGER . ' NULL',
'lft' => Schema::TYPE_INTEGER . ' NOT NULL',
'rgt' => Schema::TYPE_INTEGER . ' NOT NULL',
'depth' => Schema::TYPE_INTEGER . ' NOT NULL',
], $tableOptions);
$this->createIndex('lft', '{{%multiple_tree}}', ['tree', 'lft', 'rgt']);
$this->createIndex('rgt', '{{%multiple_tree}}', ['tree', 'rgt']);
}
}
Loading…
Cancel
Save