yujin1st/yii2-nested-set-behavior

此扩展允许您获得嵌套集合树的实用功能。

dev-master 2014-12-12 04:26 UTC

This package is auto-updated.

Last update: 2024-09-13 14:43:47 UTC


README

此扩展允许您获得嵌套集合树的实用功能。

此扩展支持 parentId 属性更新 - 这是与原 creocoder 版本的主要区别

安装

安装此扩展的首选方式是通过 composer

运行以下命令

php composer.phar require yujin1st/yii2-nested-set-behavior "*"

"yujin1st/yii2-nested-set-behavior": "*"

将以下内容添加到您的 composer.json 文件的 require 部分。

配置

首先,您需要按以下方式配置模型

class Category extends ActiveRecord
{
	public function behaviors() {
		return [
			[
				'class' => NestedSet::className(),
			],
		];
	}
	
	/**
	 * @return CategoryQuery
	 */
	public static function find() {
		return Yii::createObject(CategoryQuery::className(), [get_called_class()]);
	}

}

其次,您需要按以下方式配置查询模型

class CategoryQuery extends ActiveQuery
{
	public function behaviors() {
		return [
			[
				'class' => NestedSetQuery::className(),
			],
		];
	}
}

不需要验证在 leftAttributerightAttributerootAttributelevelAttribute 选项中指定的字段。此外,如果存在这些字段的验证规则,可能会出现问题。请检查模型 rules() 方法中是否没有提及的字段规则。

如果要在数据库中存储单个树,可以使用 schema/schema.sql 构建数据库结构。如果您打算存储多个树,则需要 schema/schema-many-roots.sql

默认情况下,leftAttributerightAttributelevelAttribute 的值与默认数据库模式中的字段名称匹配,因此您可以跳过这些配置。

此行为可以通过两种方式工作:每个表一个树和每个表多个树。根据 hasManyRoots 选项的值选择模式,默认值为 false,表示单树模式。在多树模式下,您可以将 rootAttribute 选项设置为匹配存储树的表中的现有字段。

您可以指定 parentId 属性名称 - 并在插入或移动记录时更新此属性。此功能不会影响行为本身(默认情况下已关闭),
但似乎是一个方便的方式来获取父级的主键而不需要额外的查询。

从树中选择

以下我们将使用一个示例模型 Category,其在数据库中的结构如下

- 1. Mobile phones
	- 2. iPhone
	- 3. Samsung
		- 4. X100
		- 5. C200
	- 6. Motorola
- 7. Cars
	- 8. Audi
	- 9. Ford
	- 10. Mercedes

在此示例中,我们有两个树。树根是 ID=1 和 ID=7 的记录。

获取所有根节点

使用 NestedSet::roots()

$roots = Category::find()->roots()->all();

结果

与手机和汽车节点对应的 Active Record 对象数组。

获取节点的所有后代

使用 NestedSet::descendants()

$category = Category::find(1);
$descendants = $category->descendants()->all();

结果

与 iPhone、Samsung、X100、C200 和 Motorola 对应的 Active Record 对象数组。

获取节点的所有子节点

使用 NestedSet::children()

$category = Category::find(1);
$descendants = $category->children()->all();

结果

与 iPhone、Samsung 和 Motorola 对应的 Active Record 对象数组。

获取节点的所有祖先

使用 NestedSet::ancestors()

$category = Category::find(5);
$ancestors = $category->ancestors()->all();

结果

与 Samsung 和 手机对应 的 Active Record 对象数组。

获取节点的父节点

使用 NestedSet::parent()

$category = Category::find(9);
$parent = $category->parent()->one();

结果

与 汽车 对应的 Active Record 对象数组。

获取节点同级节点

使用 NestedSet::prev()NestedSet::next()

$category = Category::find(9);
$nextSibling = $category->next()->one();

结果

与 Mercedes 对应的 Active Record 对象数组。

获取整个树

您可以使用以下标准 AR 方法获取整个树。

对于每个表一个树

Category::find()->addOrderBy('lft')->all();

对于每个表多个树

Category::find()->where('root = ?', [$root_id])->addOrderBy('lft')->all();

修改树

在本节中,我们将构建与上一节中使用的相同的树。

创建根节点

您可以使用 NestedSet::saveNode() 创建根节点。在单个表一个树模式下,您只能创建一个根节点。如果您尝试创建更多,则会抛出 CException。

$root = new Category;
$root->title = 'Mobile Phones';
$root->saveNode();
$root = new Category;
$root->title = 'Cars';
$root->saveNode();

结果

- 1. Mobile Phones
- 2. Cars

添加子节点

有多种方法可以添加子节点。有关这些方法的更多信息,请参阅API。让我们使用这些方法为我们已有的树添加节点。

$category1 = new Category;
$category1->title = 'Ford';
$category2 = new Category;
$category2->title = 'Mercedes';
$category3 = new Category;
$category3->title = 'Audi';
$root = Category::find(1);
$category1->appendTo($root);
$category2->insertAfter($category1);
$category3->insertBefore($category1);

结果

- 1. Mobile phones
	- 3. Audi
	- 4. Ford
	- 5. Mercedes
- 2. Cars

从逻辑上看,上面的树看起来不正确。我们稍后将其修复。

$category1 = new Category;
$category1->title = 'Samsung';
$category2 = new Category;
$category2->title = 'Motorola';
$category3 = new Category;
$category3->title = 'iPhone';
$root = Category::find(2);
$category1->appendTo($root);
$category2->insertAfter($category1);
$category3->prependTo($root);

结果

- 1. Mobile phones
	- 3. Audi
	- 4. Ford
	- 5. Mercedes
- 2. Cars
	- 6. iPhone
	- 7. Samsung
	- 8. Motorola
$category1 = new Category;
$category1->title = 'X100';
$category2 = new Category;
$category2->title = 'C200';
$node = Category::find(3);
$category1->appendTo($node);
$category2->prependTo($node);

结果

- 1. Mobile phones
	- 3. Audi
		- 9. С200
		- 10. X100
	- 4. Ford
	- 5. Mercedes
- 2. Cars
	- 6. iPhone
	- 7. Samsung
	- 8. Motorola

修改树

在本节中,我们最终将使我们的树变得逻辑化。

树修改方法

有几个方法允许您修改树。有关这些方法的更多信息,请参阅API。

让我们开始吧

// move phones to the proper place
$x100 = Category::find(10);
$c200 = Category::find(9);
$samsung = Category::find(7);
$x100->moveAsFirst($samsung);
$c200->moveBefore($x100);
// now move all Samsung phones branch
$mobile_phones = Category::find(1);
$samsung->moveAsFirst($mobile_phones);
// move the rest of phone models
$iphone = Category::find(6);
$iphone->moveAsFirst($mobile_phones);
$motorola = Category::find(8);
$motorola->moveAfter($samsung);
// move car models to appropriate place
$cars = Category::find(2);
$audi = Category::find(3);
$ford = Category::find(4);
$mercedes = Category::find(5);

foreach([$audi, $ford, $mercedes] as $category) {
	$category->moveAsLast($cars);
}

结果

- 1. Mobile phones
	- 6. iPhone
	- 7. Samsung
		- 10. X100
		- 9. С200
	- 8. Motorola
- 2. Cars
	- 3. Audi
	- 4. Ford
	- 5. Mercedes

移动节点使其成为新的根节点

存在一个特殊的moveAsRoot()方法,允许移动节点并将其作为新的根节点。在这种情况下,所有后代也会一起移动。

示例

$node = Category::find(10);
$node->moveAsRoot();

识别节点类型

有三种方法可以获取节点类型:isRoot()isLeaf()isDescendantOf()

示例

$root = Category::find(1);
VarDumper::dump($root->isRoot()); //true;
VarDumper::dump($root->isLeaf()); //false;
$node = Category::find(9);
VarDumper::dump($node->isDescendantOf($root)); //true;
VarDumper::dump($node->isRoot()); //false;
VarDumper::dump($node->isLeaf()); //true;
$samsung = Category::find(7);
VarDumper::dump($node->isDescendantOf($samsung)); //true;

有用代码

非递归树遍历

$categories = Category::find()->addOrderBy('lft')->all();
$level = 0;

foreach ($categories as $n => $category)
{
	if ($category->level == $level) {
		echo Html::endTag('li') . "\n";
	} elseif ($category->level > $level) {
		echo Html::beginTag('ul') . "\n";
	} else {
		echo Html::endTag('li') . "\n";

		for ($i = $level - $category->level; $i; $i--) {
			echo Html::endTag('ul') . "\n";
			echo Html::endTag('li') . "\n";
		}
	}

	echo Html::beginTag('li');
	echo Html::encode($category->title);
	$level = $category->level;
}

for ($i = $level; $i; $i--) {
	echo Html::endTag('li') . "\n";
	echo Html::endTag('ul') . "\n";
}

使用parentId属性构建两级树

$categories = Category::find()->andWhere('level<3')->all();
$list = [];

foreach ($categories as $category)
{
  // if record is root it may have the **null** value (for correct foreign keys)
  // but you can specify this in  **emptyParentAttributeValue** property 
  $list[$category->parentId ? $category->parentId : 0][] = $category;
}

foreach ($list[0] as $category)
{
	echo $category->title;
	foreach ($list[$category->id] as $subCategory){
		echo ' - ' . $subCategory->title;
	}
}