PaulZi
10 years ago
commit
7f8633ea98
8 changed files with 1030 additions and 0 deletions
@ -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 |
@ -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. |
||||||
|
|
@ -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)]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
``` |
@ -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\\": "" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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']); |
||||||
|
} |
||||||
|
} |
@ -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…
Reference in new issue