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