mgrechanik/yii2-materialized-path

为Yii2框架的Materialized Path行为

安装: 701

依赖: 1

建议者: 0

安全: 0

星标: 6

关注者: 1

分支: 0

开放问题: 0

类型:yii2-extension

1.0.0 2019-06-28 07:02 UTC

This package is auto-updated.

Last update: 2024-09-15 23:13:59 UTC


README

此扩展允许根据Materialized Path算法将Active Record模型组织成树结构

俄语版本

目录

功能

  • 允许将Active Record模型组织成树
  • 每个树只有一个根节点
  • 可以在一个数据库表中保留多个独立的树,例如保留多个菜单的菜单项
  • 多种方法在树的节点之间导航以及查询节点的属性
  • 树修改操作:插入新节点,移动现有节点。如果请求,它们在transaction中运行
  • 删除节点的两种模式:当删除节点时与其子节点一起删除或将其移动到其父节点
  • 管理树的允许服务
    • 通过一个数据库查询查询树(或子树)
    • 树以两种格式形成:嵌套结构,适用于显示为<ul>-<li>列表,或“平面”表示的树 - 简单的php数组,适用于显示在<select>列表中或用作数据提供者
    • 克隆子树
    • 获取节点的子节点的id范围,这对于验证规则很有用

安装

安装此扩展的最佳方式是通过composer

运行以下命令:

composer require --prefer-dist mgrechanik/yii2-materialized-path

或者添加以下内容到你的composer.json文件中的require部分:

"mgrechanik/yii2-materialized-path" : "^1.0"

迁移

此扩展期望数据库表中具有保留树所必需的额外列。

多树表的迁移示例请参考这里

单树表的迁移示例请参考这里

use yii\db\Migration;

/**
 * Handles the creation of table `animal`.
 */
class m170208_094404_create_animal_table extends Migration
{
    /**
     * @inheritdoc
     */
    public function up()
    {
        $this->createTable('animal', [
            'id' => $this->primaryKey(),
            'path' => $this->string(255)->notNull()->defaultValue('')->comment('Path to parent node'),
            'level' => $this->integer(4)->notNull()->defaultValue(1)->comment('Level of the node in the tree'),
            'weight' => $this->integer(11)->notNull()->defaultValue(1)->comment('Weight among siblings'),
            'name' => $this->string()->notNull()->comment('Name'),
        ]);
        
        $this->createIndex('animal_path_index', 'animal', 'path');
        $this->createIndex('animal_level_index', 'animal', 'level');
        $this->createIndex('animal_weight_index', 'animal', 'weight');        
        
    }

    /**
     * @inheritdoc
     */
    public function down()
    {
        $this->dropTable('animal');
    }
}

上述代码中的要点

  1. 字段角色的说明将在数据结构说明中给出
  2. 对于path,我们将字段长度设置为255个符号,这对于mysql是最佳的,允许保留具有巨大嵌套的树,但您可以设置任何想要的值
  3. pathweightlevel字段的defaultValue已设置,以防万一,即使不是通过此扩展的api而是手动(例如使用phpmyadmin)添加的行也能在树中占据其起始位置
  4. 对于SQLite数据库,请从上述迁移中移除->comment()

设置

要将Active Record模型转换为树节点,您需要为其设置以下行为:

use mgrechanik\yiimaterializedpath\MaterializedPathBehavior;

class Animal extends \yii\db\ActiveRecord
{
    public static function tableName()
    {
        return 'animal';
    }
	
    public function behaviors()
    {
        return [
            'materializedpath' => [
                'class' => MaterializedPathBehavior::class,
                // behavior settings
            ],
        ];
    } 
	
	// ...
}	

MaterializedPathBehavior行为有以下设置:

  1. treeIdentityFields - 包含树唯一标识符的字段名的数组(当存在多个时)。
  2. mpFieldNames - 用于此扩展的字段名与模型类中的字段名的映射。当您的名称与idpathlevelweight不同时使用。
  3. modelScenarioForAffectedModelsForSavingProcess的解释在这里
  4. 关于moveChildrenWnenDeletingParentmodelScenarioForChildrenNodesWhenTheyDeletedAfterParent,请参阅删除节点
  5. maxPathLength可以任意设置为path字段的限制值,当它变长时将抛出异常。

数据结构说明

数据库中的数据如下所示

当表只包含一个树时: 数据库中的树表示

当表包含多个树时: 数据库中的树表示

以下示例将使用上面的树。
对于第一个表,有Animal Active Record模型。
对于第二个表,有Menuitem Active Record模型。

常见问题

  • 如果表包含多个树,如Menuitem表,我们将使用额外的列(例如treeid)作为唯一树标识符。并且所有未来的树操作都将隔离,它们只涉及这个具体的树,其他树则不在考虑范围内。
  • 每个树只有一个根节点,它不包含在表中,它是一个虚拟节点,但您可以通过与任何其他节点相同的方式获取它并与之交互。更多详细信息
  • 节点Animal(1) - cat是此根节点的第一个子节点
  • id列 - 它持有节点的唯一数值标识符。此扩展要求使用正整数作为节点的id
  • path列 - 它持有此节点的父节点路径。以节点的id的形式,由/分隔。根节点的子节点为空字符串。
  • level列 - 它持有此节点的级别,根节点的级别为0
  • weight列 - 它持有节点在兄弟节点中的“权重”。相邻节点按此列排序。

因此,这种架构保证了将树保存到数据库表中的有效和足够结构。

  • 如果需要,我们保留一个标记,指明节点属于哪个树。
  • 每个节点都通过path知道其父节点。
  • 在兄弟节点之间,节点根据weight排序。

与其他流行算法实现的区别

  1. 在其他扩展中,如Animal(1) - cat这样的节点通常被视为根节点。这看起来像是一个树有多个根节点。
    在我们的扩展中,每个树只有一个根节点(它不保存到数据库中)。
    这样,所有树节点都组织在一个树中。
  2. 此外,在path列中通常保存的是完整路径 - 以末尾节点id结尾的路径。
    在这个扩展中,我们只保存节点父级的路径。对于兄弟节点,这个值将是相同的

扩展结构

此扩展提供了两个主要功能

  • mgrechanik\yiimaterializedpath\MaterializedPathBehavior - 此行为连接到ActiveRecord模型,通过向其中添加必要的功能,将其转换为树节点
  • mgrechanik\yiimaterializedpath\Service - 此服务旨在提供管理树形结构的额外操作:构建和输出树(子树)、获取根节点、克隆等。

处理树形结构

树的根节点

常见

每个树只有一个根节点(以后简称“根”) - 位于树顶部的节点,没有父节点。

我们不会为这个根节点在数据库中保存一行,因为我们已经知道每个树都有它。因此,我们不需要在表中额外创建它以开始填充树节点。
我们认为根节点已经存在。
在数据库中,我们只保存真正添加到树中的数据。从第一级节点开始 - catdogsnakebear

然而,我们可以像处理任何其他树节点一样处理这个根节点

  • 我们可以要求它的后代,这样我们就能得到整个树
  • 我们可以向它添加节点,它们将成为第一级节点
  • 但在根节点的情况下,只有逻辑上的事情起作用。我们可以向根节点添加节点,但不能在根节点之前或之后插入

技术上,根节点由mgrechanik\yiimaterializedpath\tools\RootNode类的对象表示 - 这是一个一些虚拟 AR模型,你不应该尝试将其保存到数据库中(将引发异常),但它也被配置了MaterializedPathBehavior,这允许像处理任何其他节点一样处理它。

根节点的ID

正如我们之前所说的,在path字段中,我们不保存根的id(因为它在表中不存在),但根节点仍然有它的id - 它在RootNode::getId()中受到控制。我们主要需要它用于编辑html表单时,我们可能需要在表单中选择根节点,并且我们需要一种方法通过标识符来区分它和其他节点。

此根节点的id是根据一个简单的算法形成的

  • 它将是一个负整数
  • 如果表中只有一个树,它的值将是-100
  • 如果有许多树在表中,它的值是通过以下公式计算的 - -100 * (treeField1 + treeField1 + treeFieldi),因此对于['treeid' => 2]id将是-200

处理根节点

要处理根节点,我们首先需要获取它的对象。
这可以通过几种方式完成

  1. 通过AR模型名称(和任意树条件)
use mgrechanik\yiimaterializedpath\ServiceInterface;
// AR class
use mgrechanik\yiimaterializedpath\tests\models\Animal;
// service of managing the trees
$service = \Yii::createObject(ServiceInterface::class);
// getting
// the root of the tree
$root = $service->getRoot(Animal::class);  

如果你的表包含多个树,为了获取所需树的根,你需要提供该树的树条件

use mgrechanik\yiimaterializedpath\tests\models\Menuitem;
// ...
// the root of the first tree
$root1 = $service->getRoot(Menuitem::class, ['treeid' => 1]);
// the root of the second tree
$root2 = $service->getRoot(Menuitem::class, ['treeid' => 2]);
  1. 通过任何AR模型(不是新记录)我们可以获取此节点所属树根节点
$model7 = Animal::findOne(7); // 'stag'
// the root of the tree to which $model7 belongs to
$root = $model7->getRoot();

对于树中的所有节点,缓存将返回相同的根对象(你可以使用===运算符进行比较)。

  1. 通过它的负id
$root = $service->getModelById(Animal::class, -100); 

当你在html表单中获取id值时,其中根节点和普通节点都可以选择,请使用此方法获取节点。

示例:12

查询

选择任何节点后,包括根节点,您都可以请求下一级信息

  1. 获取节点的子节点

获取节点子节点的查询对象

               $node->getDescendantsQuery($exceptIds = [], $depth = null)
  • 通过$exceptIds分别告知您想从查询中排除哪些子树,其格式如下:[1, 2, 3 => false],表示排除节点12的子树,但保留节点3及其所有子节点
  • $depth显示要查询的子节点层级数
    null表示查询所有子节点
    1表示仅查询1级深度,换句话说,仅查询子节点
    依此类推。

示例

$model = Animal::findOne(1);
$query = $model->getDescendantsQuery();
$result = $query->asArray()->all();

我们得到的结果将按level ASC, weight ASC排序,因此它不适合任何树形结构的输出。
有关构建php树的更多信息,请参阅此处

  1. 获取查询对象

当您需要构建自己的查询以获取树节点而不是使用AR模型(如ClassName::find())时,您应该从以下查询对象开始

               $node->getQuery()
  • 此对象持有属于此$node的树的tree condition
  • 所有此扩展的查询都从该查询对象开始
  • 您还可以从根节点获取它
  • 从技术上讲,您可以在RootNode::getQuery()中查看其实现
  • 您现在需要开始使用andWhere()添加新条件

导航

  1. 获取树的根节点
               $node->getRoot()

它仅适用于存在于数据库模型中的节点,因为新模型尚未属于任何树。
它为每个树节点返回相同的对象,因为树只有一个根节点

  1. 处理节点的子节点

获取所有节点的子节点

               $node->children()

我们将获得AR模型的数组 - 节点的直接子节点。

或者您可以使用更常见的查询对象

               $node->getChildrenQuery($sortAsc = true)

示例

$query = $model->getChildrenQuery();
$result = $query->asArray()->all();

获取节点的第一个子节点

               $node->firstChild()

获取节点的最后一个子节点

               $node->lastChild()
  1. 处理节点的父节点

获取父节点

               $node->parent()

获取父节点

               $node->parents($orderFromRootToCurrent = true, $includeRootNode = false, $indexResultBy = false)

在这里

  • $orderFromRootToCurrent - 从根节点到当前节点或相反排序父节点
  • $includeRootNode - 是否在结果中包含根节点
  • $indexResultBy - 结果是否应按模型的id进行索引

获取父节点的id

               $node->getParentIds($includeRootNode = false)

在这里

  • 结果将按从根节点到当前节点父节点的顺序显示节点的id
  • $includeRootNode - 是否在结果中包含根节点的id
  1. 处理节点的兄弟节点

获取

所有兄弟节点

               $node->siblings($withCurrent = false, $indexResultBy = false)

在这里

  • $withCurrent - 是否在结果中包含当前节点
  • $indexResultBy - 结果是否应按模型的id进行索引

下一个

               $node->next()

上一个

               $node->prev()

所有下一个

               $node->nextAll()

所有上一个

               $node->prevAll()

获取当前节点在兄弟节点中的位置(从0开始)

               $node->index()	

节点属性

您可以在节点上执行以下检查

检查节点是否为根节点

               $node->isRoot()

检查节点是否为叶子节点,即它没有任何子节点

               $node->isLeaf()

检查节点是否是另一个节点的子节点

               $node->isDescendantOf($node)

      ,这里的$node参数可以是ActiveRecord对象或数字 - 节点的主键

检查节点是否是另一个节点的子节点

               $node->isChildOf($node)

      ,参数$node如上所述

检查节点是否是另一个节点的兄弟节点

               $node->isSiblingOf($node)

      ,参数$node只能是ActiveRecord模型

关于节点的更多信息

节点的完整路径

               $node->getFullPath()

      , 此路径包括我们持有的 path 字段与 id 的连接
的当前节点。例如,对于 Animal(5) 节点,此值将是 '1/5'

节点 ID

               $node->getId()

      , 此包装器很有用,因为它也适用于根节点(给出负 id

节点级别

               $node->getLevel()

此节点所属的树的条件

               $node->getTreeCondition()

      , 它将为 Menuitem 表返回一个类似 ['treeid' => 2] 的数组

获取此扩展使用的字段名称

               $node->getIdFieldName()
               $node->getPathFieldName()
               $node->getLevelFieldName()
               $node->getWeightFieldName()

插入新节点和移动现有节点

常见信息

关于插入/移动节点您需要了解的常见信息

  1. 这些方法对插入新节点和移动现有节点都有效
  2. 每个修改方法的签名如下
    • $node->appendTo($node, $runValidation = true, $runTransaction = true)
    • 这些方法物理保存模型,因此它们作为 ActiveRecord::save() 工作,这意味着可以在保存之前使用 $runValidation 进行验证检查
    • 它们返回 true/false 以表示操作是否成功
    • 通常,插入/移动操作会导致某些其他节点需要更改并保存,因此使用 $runTransaction 在一个事务中运行所有这些操作很方便。
    • 此外,还有设置 MaterializedPathBehavior::$modelScenarioForAffectedModelsForSavingProcess,允许在保存之前为这些额外更改的模型设置一些 场景
  3. 在这些操作中,根节点 只能在添加/移动节点到它的操作中使用
  4. 所有如 prependTo/insertBefore(After) 的操作都需要重建所有新兄弟的 weight 字段。为了解决这个任务,此扩展不执行搜索 weight 的空闲间隔或类似的工作,它重建所有新兄弟的“权重”并将它们全部保存
  5. 看到表中有很多树的情况
    • 当创建新节点时,它们的 树条件 将与我们要添加新节点的相关节点相同
    • 不允许将现有节点移动到另一棵树

因此,以下操作

将节点添加/移动到新父节点最后子节点的位置

               $model->appendTo($node, $runValidation = true, $runTransaction = true)
  • $model 将被添加/移动到新父节点 $node,位于最后子节点的位置
  • 在移动时,所有 $model 的后代将自然地与之移动

有一个“镜像”方法用于上述方法,当我们可以向左节点添加子节点时

               $model->add($child, $runValidation = true, $runTransaction = true)

示例:123

将节点添加/移动到新父节点第一个子节点的位置

               $model->prependTo($node, $runValidation = true, $runTransaction = true)
  • $model 将被添加/移动到新父节点 $node,位于第一个子节点的位置

示例

将节点添加/移动到另一个节点之前的位置

               $model->insertBefore($node, $runValidation = true, $runTransaction = true)
  • $model 将被添加/移动到 $node 之前的位置。它们的父节点将是相同的

示例.

将节点添加/移动到另一个节点之后的位置

               $model->insertAfter($node, $runValidation = true, $runTransaction = true)
  • $model将被添加/移动到$node` 之后的位置

示例.

将节点添加/移动到新父节点指定为数字的位置

               $model->insertAsChildAtPosition($node, $position, $runValidation = true, $runTransaction = true)
  • $model 将被添加/移动到新父节点 $node,并在其子节点中占据 $position 位置(从零开始计数)
  • 技术上,这是上述方法的包装器

示例

删除节点

通过在 Yii 中存在的 ActiveRecord::delete() 方法来删除节点

               $model->delete()

在删除节点时,下一个 MaterializedPathBehavior::$moveChildrenWnenDeletingParent 行为设置提供了两种选择,用于节点后代

  1. true - 被删除节点的后代将被移动到其父节点(通过appendTo方式)。这是默认设置。
  2. false - 后代将与节点一起被删除

因为delete()方法是框架的内部方法,如果您想它在事务中运行,您需要自己设置以下内容,请参考文档。请查看目录模型中的此设置示例。

在目录模型示例中,删除操作被包含在事务中,但如果需要删除后代,我们不希望在嵌套事务中运行所有这些额外的删除操作,因此通过使用设置MaterializedPathBehavior::$modelScenarioForChildrenNodesWhenTheyDeletedAfterParent,我们可以在删除这些后代节点之前为它们设置一些场景(不同于在transactions()中设置的场景)。

示例

服务用于管理树

常见

此服务在需要操作多个节点时,为管理树提供了额外的功能。

我们可以通过以下方式获取此服务

use mgrechanik\yiimaterializedpath\ServiceInterface;
// trees managing service
$service = \Yii::createObject(ServiceInterface::class);

或使用DI注入。
从技术上讲,此服务的单例在扩展的引导程序中定义:mgrechanik\yiimaterializedpath\tools\Bootstrap

构建树及其输出

常见信息

如上所述,$node->getDescendantsQuery()查询(适用于root node,如果需要整个树)通过一个数据库查询获取所需数量的节点后代。但是,我们需要将此查询结果数组转换为便于工作(以及显示)的树形结构。

因此,在此服务中,我们将此结构转换为两种类型的php

  • buildTreebuildDescendantsTree构建以节点形式连接在一起的特殊类型的层次结构。这种结构便于以嵌套的<ul>-<li>列表的形式递归输出树。
  • buildFlatTree根据上述信息构建“平面”树 - 简单的一维节点数组,根据它们的顺序排列。它便于在管理员页面使用单个foreach输出树,用于<select>列表或数据提供者。

层次树

               $service->buildTree($parent, $isArray = true, $exceptIds = [], $depth = null)
               $service->buildDescendantsTree($parent, $isArray = true, $exceptIds = [], $depth = null)

这些方法将构建以下树:

  1. 常见算法如下
    • 选择我们将从其中开始我们的后代树的节点
    • 构建树
    • 打印它
  2. 结果将是mgrechanik\yiimaterializedpath\tools\TreeNode类型的对象数组,这些对象在children属性中引用其子节点
  3. buildTree$parent节点开始构建树,而buildDescendantsTree则从$parent节点的子节点开始构建树
  4. isArray - 选择我们将数据放入TreeNode::$node的格式(数组或AR对象)
  5. $exceptIds请参阅以上
  6. $depth请参阅以上
  7. 可以使用简单的小部件,如mgrechanik\yiimaterializedpath\widgets\TreeToListWidget,以嵌套的<ul>-<li>列表的形式打印此树。此小部件功能非常基本,主要作为示例,但仍有机会创建您可能需要的任何树项标签

示例

use mgrechanik\yiimaterializedpath\ServiceInterface;
use mgrechanik\yiimaterializedpath\tests\models\Animal;
use mgrechanik\yiimaterializedpath\widgets\TreeToListWidget;

$service = \Yii::createObject(ServiceInterface::class);

// 1) choose the model
$model1 = Animal::findOne(1);
// 2) build the tree
$tree = $service->buildTree($model1);

我们将得到如下结构

Array
(
    [0] => TreeNode Object
        (
            [node] => Array ([id] => 1, [name] => cat, ...))    // <---- The very node with id=1
            [parent] => 
            [children] => Array                                 // <---- This is it's children (ARRAY-Z)
                (
                    [0] => TreeNode Object
                        (
                            [node] => Array ([id] => 5, [name] => mouse, ...)
                            [parent] => ...
                            [children] => Array
                                (
                                    [0] => TreeNode Object
                                        (
                                            [node] => Array ([id] => 7, [name] => stag, ...)
                                            [parent] => ...
                                            [children] => Array ()
                                        )
                                )
                        )
                    [1] => TreeNode Object
                        (
                            [node] => Array ( [id] => 6, [name] => fox, ... )
                            [parent] => ...
                            [children] => Array ()
                        )
                )
        )
)

如果树以这种方式构建 - $tree = $service->buildDescendantsTree($model1);,则结果将是上面的ARRAY-Z

// 3) print the tree:
print TreeToListWidget::widget(['tree' => $tree]);

我们将得到

    • 老鼠
      • 麋鹿
    • 狐狸

上述代码的HTML如下

<ul>
    <li>cat
        <ul>
            <li>mouse
                <ul>
                    <li>stag</li>
                </ul>
            </li>
            <li>fox</li>
        </ul>
    </li>
</ul>

全部树的输出示例

代码

$root = $service->getRoot(Animal::class);
$tree = $service->buildDescendantsTree($root);
print TreeToListWidget::widget(['tree' => $tree]);

我们将得到

    • 老鼠
      • 麋鹿
    • 狐狸
    • 狮子
    • 刺猬

树的前两级输出的示例(使用$depth参数)

代码

$root = $service->getRoot(Animal::class);
$tree = $service->buildDescendantsTree($root, true, [], 2);
print TreeToListWidget::widget(['tree' => $tree]);

将打印

    • 老鼠
    • 狐狸
    • 狮子
    • 刺猬

扁平树

术语“扁平”意味着一种简单数组的树形式,可以通过一个foreach循环以如下形式输出

- root             
  - (1) cat        
    -- (5) mouse   
      --- (7) stag 
    -- (6) fox     
  - (2) dog        
  - (3) snake      
    -- (8) lion    
    -- (9) hedgehog
  - (4) bear 

此树是这样创建的

           $service->buildFlatTree($parent, $isArray = true, $includeItself = false, $indexBy = false, $exceptIds = [], $depth = null)

此方法将构建以下树:

  1. 常见算法如下
    • 选择我们将从其中开始我们的后代树的节点
    • 构建树
    • 打印它
  2. 结果将是表示为关联数组的节点数组($isArray = true)或AR对象
  3. $includeItself设置从哪里开始我们的树 - 当$includeItself = true时从$parent开始,或从它的子节点开始
  4. $indexBy - 是否使用模型的id来索引结果数组。与数据提供程序一起使用时非常有用
  5. $exceptIds上面
  6. $depth上面

示例

// 1) choose the node
$root = $service->getRoot(Animal::class);
// 2) build the tree
$tree = $service->buildFlatTree($root);

我们将得到以下数组

Array
(
    [0] => Array               // To make the keys equal to ids look at $indexBy above
        (
            [id] => 1
            [path] => 
            [level] => 1
            [weight] => 1
            [name] => cat
        )
    [1] => Array
        (
            [id] => 5
            [path] => 1/
            [level] => 2
            [weight] => 1
            [name] => mouse
        )
    [2] => Array
        (
            [id] => 7
            [path] => 1/5/
            [level] => 3
            [weight] => 1
            [name] => stag
        )
    // .....
    // .....
    [8] => Array
        (
            [id] => 4
            [path] => 
            [level] => 1
            [weight] => 4
            [name] => bear
        )
)

此数组(当与$indexBy一起使用时)准备好提供给数据提供程序,例如,查看目录视图页面 - actionIndex - 在目录控制器中。

要将此数组转换为$items以供Yii的内置listBox使用,我们需要以下助手

           $service->buildSelectItems($flatTreeArray, callable $createLabel, $indexKey = 'id', $isArray = true);
  1. $flatTreeArray - 通过buildFlatTree构建的数组
  2. $createLabel - 创建项目标签的匿名函数。此函数接收处理过的节点并返回字符串 - 我们在<select>列表中看到的项标签
  3. $indexKey - 使用哪个字段来索引选择项
  4. 结果将是选项数组[id1 => label1, id2 => label2, ...]
// 3) Build select list
$items = $service->buildSelectItems($tree,
	function($node) {
		return ($node['id'] < 0) ? '- root' : str_repeat('  ', $node['level']) . str_repeat('-', $node['level']) . 
				' (' . $node['id'] . ') ' . $node['name'];
	}
); 

这将生成以下列表

  - (1) cat        
    -- (5) mouse   
      --- (7) stag 
    -- (6) fox     
  - (2) dog        
  - (3) snake      
    -- (8) lion    
    -- (9) hedgehog
  - (4) bear 

全部树的输出示例,包括

$root = $service->getRoot(Animal::class);
$tree = $service->buildFlatTree($root, true, true);
$items = $service->buildSelectItems($tree,
	function($node) {
		return ($node['id'] < 0) ? '- root' : str_repeat('  ', $node['level']) . str_repeat('-', $node['level']) . 
				' (' . $node['id'] . ') ' . $node['name'];
	} 
); 

我们将有

- root             
  - (1) cat        
    -- (5) mouse   
      --- (7) stag 
    -- (6) fox     
  - (2) dog        
  - (3) snake      
    -- (8) lion    
    -- (9) hedgehog
  - (4) bear 

您可以在目录元素编辑表单中看到此代码的工作示例。

克隆

           $service->cloneSubtree($sourceNode, $destNode, $withSourceNode = true, $scenario = null)
  1. 常见
    • 此操作将在transaction中执行
    • 克隆只能在同一类型的模型之间进行
    • 您可以将克隆到另一个树中
    • $sourceNode$destNode - Active Record模型或根节点
  2. $sourceNode - 我们正在克隆的子树的根
  3. $destNode - 我们正在克隆到的节点
  4. $withSourceNode - 是否从$sourceNode本身开始克隆或从它的子节点开始
    例如,如果$sourceNode,则需要将其设置为false。整个树将被克隆
  5. $scenario - 可选地,我们可以在保存之前将scenario设置到克隆的节点上

克隆示例

其他机会

获取树的根

           $service->getRoot($className, $treeCondition = [])
  1. $className - ActiveRecord模型名称
  2. $treeCondition - 当表包含多个树时,树条件。它是一个如['treeid' => 1]的数组

通过其id获取任何节点

           $service->getModelById($className, $id, $treeCondition = [])
  1. 这是一个包装器,用于$className::findOne($id),它能够通过其负$id找到根节点。它用于当我们有一个html表单,并且根节点可以与其它节点一起选择时
  2. $className - ActiveRecord模型名称
  3. $id - 模型的唯一标识符或作为根的id的负数
  4. $treeCondition - 当表包含多个树时的树条件。只有当树条件由多个字段组成时,才需要提供它。对于只有一个字段的条件,如 - ['treeid' => 2],省略此参数,因为它可以从$id中推断出来。

获取某些子树的节点id

           $service->buildSubtreeIdRange($parent, $includeItself = false, $exceptIds = [], $depth = null)
  1. 允许获取$parent节点特定后代的节点id数组
  2. $includeItself - 是否包含$parentid
  3. $exceptIds上面
  4. $depth上面
  5. 此功能与yii\validators\RangeValidator一起使用很有趣

节点的树条件

           $service->getTreeCondition($model)
  1. $model - 我们正在检查的节点
  2. 它将返回一个数组,如['treeid' => 1] - 通过该数组,$model节点属于其树

从路径中获取父级的id

           $service->getParentidFromPath($path)
  1. $path - 路径
  2. 它将返回路径中的最后一个id或如果路径为空则返回null

附录A:构建目录示例

有关如何在admin页面创建/编辑树节点并显示树的示例,请参阅在Yii2中创建目录一文,您可以在其中看到所有这些架构的工作情况。

附录B:API操作示例

常见

所有示例都将与以下起始状态的Animal表一起工作

- root             
  - (1) cat        
    -- (5) mouse   
      --- (7) stag 
    -- (6) fox     
  - (2) dog        
  - (3) snake      
    -- (8) lion    
    -- (9) hedgehog
  - (4) bear 

所有代码示例的下一个起始状态也是隐式的

use mgrechanik\yiimaterializedpath\ServiceInterface;
use mgrechanik\yiimaterializedpath\tests\models\Animal;
// tree managing service
$service = \Yii::createObject(ServiceInterface::class);

处理根节点

使用add()appendTo()向根节点添加新节点

是否这种方式

$root = $service->getRoot(Animal::class);
$root->add(new Animal(['name' => 'new']));

或另一种方式

$root = $service->getRoot(Animal::class);
$newModel = new Animal(['name' => 'new']);
$newModel->appendTo($root);

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                  --- (7) stag
    -- (6) fox                    -- (6) fox
  - (2) dog            ==>      - (2) dog
  - (3) snake                   - (3) snake
    -- (8) lion                   -- (8) lion
    -- (9) hedgehog               -- (9) hedgehog
  - (4) bear                    - (4) bear
                                - (10) new      

使用appendTo()将现有节点移动到根位置

$model7 = Animal::findOne(7);
$root = $model7->getRoot();
$model7->appendTo($root);

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                -- (6) fox
    -- (6) fox                  - (2) dog
  - (2) dog            ==>      - (3) snake
  - (3) snake                     -- (8) lion
    -- (8) lion                   -- (9) hedgehog
    -- (9) hedgehog             - (4) bear
  - (4) bear                    - (7) stag

appendTo()

将子树移动到新位置

$model1 = Animal::findOne(1);
$model3 = Animal::findOne(3);
$model1->appendTo($model3);

以下更改将发生

- root                        - root
  - (1) cat                     - (2) dog
    -- (5) mouse                - (3) snake
      --- (7) stag                -- (8) lion
    -- (6) fox                    -- (9) hedgehog
  - (2) dog            ==>        -- (1) cat
  - (3) snake                       --- (5) mouse
    -- (8) lion                       ---- (7) stag
    -- (9) hedgehog                 --- (6) fox
  - (4) bear                    - (4) bear

prependTo()

将新节点作为另一个节点的第一个子节点添加

$model1 = Animal::findOne(1);
$newModel = new Animal(['name' => 'new']);
$newModel->prependTo($model1);

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (12) new
      --- (7) stag                -- (5) mouse
    -- (6) fox                      --- (7) stag
  - (2) dog            ==>        -- (6) fox
  - (3) snake                   - (2) dog
    -- (8) lion                 - (3) snake
    -- (9) hedgehog               -- (8) lion
  - (4) bear                      -- (9) hedgehog
                                - (4) bear

insertBefore()

将新节点添加到另一个节点之前

$model3 = Animal::findOne(3);
$newModel = new Animal(['name' => 'new']);
$newModel->insertBefore($model3);

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                  --- (7) stag
    -- (6) fox                    -- (6) fox
  - (2) dog            ==>      - (2) dog
  - (3) snake                   - (13) new
    -- (8) lion                 - (3) snake
    -- (9) hedgehog               -- (8) lion
  - (4) bear                      -- (9) hedgehog
                                - (4) bear

insertAfter()

将现有节点移动到另一个节点之后

$model7 = Animal::findOne(7);
$model8 = Animal::findOne(8);
$model7->insertAfter($model8);

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                -- (6) fox
    -- (6) fox                  - (2) dog
  - (2) dog            ==>      - (3) snake
  - (3) snake                     -- (8) lion
    -- (8) lion                   -- (7) stag
    -- (9) hedgehog               -- (9) hedgehog
  - (4) bear                    - (4) bear

insertAsChildAtPosition()

将新模型作为根的第三个子节点插入(位置2

$root = $service->getRoot(Animal::class);
$newModel = new Animal(['name' => 'new']);
$newModel->insertAsChildAtPosition($root, 2);

以下更改将发生

- root                        - root                        
  - (1) cat                     - (1) cat                   
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                  --- (7) stag
    -- (6) fox                    -- (6) fox
  - (2) dog            ==>      - (2) dog                   
  - (3) snake                   - (14) new                  
    -- (8) lion                 - (3) snake
    -- (9) hedgehog               -- (8) lion
  - (4) bear                      -- (9) hedgehog
                                - (4) bear

delete()

删除现有节点及其后代移动到其父节点

$model3 = Animal::findOne(3);
$model3->delete()

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                  --- (7) stag
    -- (6) fox                    -- (6) fox
  - (2) dog            ==>      - (2) dog
  - (3) snake                   - (4) bear
    -- (8) lion                 - (8) lion
    -- (9) hedgehog             - (9) hedgehog
  - (4) bear                  

克隆

克隆一个节点

$model7 = Animal::findOne(7);
$model8 = Animal::findOne(8);
$service->cloneSubtree($model7, $model8);

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                  --- (7) stag
    -- (6) fox                    -- (6) fox
  - (2) dog            ==>      - (2) dog
  - (3) snake                   - (3) snake
    -- (8) lion                   -- (8) lion
    -- (9) hedgehog                 --- (197) stag
  - (4) bear                      -- (9) hedgehog
                                - (4) bear                

克隆整个子树

克隆所有子树(默认模式)

$model1 = Animal::findOne(1);
$model8 = Animal::findOne(8);
$service->cloneSubtree($model1, $model8);

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                  --- (7) stag
    -- (6) fox                    -- (6) fox
  - (2) dog            ==>      - (2) dog
  - (3) snake                   - (3) snake
    -- (8) lion                   -- (8) lion
    -- (9) hedgehog                 --- (198) cat
  - (4) bear                          ---- (199) mouse
                                        ----- (200) stag
                                      ---- (201) fox
                                  -- (9) hedgehog
                                - (4) bear               

克隆不包含根的子树

从源节点的子节点开始克隆子树

$model1 = Animal::findOne(1);
$model8 = Animal::findOne(8);
$service->cloneSubtree($model1, $model8, false);

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                  --- (7) stag
    -- (6) fox                    -- (6) fox
  - (2) dog            ==>      - (2) dog
  - (3) snake                   - (3) snake
    -- (8) lion                   -- (8) lion
    -- (9) hedgehog                 --- (202) mouse
  - (4) bear                          ---- (203) stag
                                    --- (204) fox
                                  -- (9) hedgehog
                                - (4) bear              

克隆节点的后代

$model1 = Animal::findOne(1);
$service->cloneSubtree($model1, $model1, false);

以下更改将发生

- root                        - root
  - (1) cat                     - (1) cat
    -- (5) mouse                  -- (5) mouse
      --- (7) stag                  --- (7) stag
    -- (6) fox                    -- (6) fox
  - (2) dog            ==>        -- (205) mouse
  - (3) snake                       --- (206) stag
    -- (8) lion                   -- (207) fox
    -- (9) hedgehog             - (2) dog
  - (4) bear                    - (3) snake
                                  -- (8) lion
                                  -- (9) hedgehog
                                - (4) bear             

将整个树克隆到新树中

显示Menuitem表的原状态此处
将所有树节点克隆到新树中

// root of not empty tree
$root1 = $service->getRoot(Menuitem::class, ['treeid' => 1]);
// root of new and empty tree
$root5 = $service->getRoot(Menuitem::class, ['treeid' => 5]);
// cloning by starting from root1's children
$service->cloneSubtree($root1, $root5, false);

以下更改将发生

- root1                     
  - (1) red                   
    -- (4) black              
    -- (5) yellow    ==>      ...   
  - (2) green                
    -- (6) blue              
  - (3) brown                
  
- root5                     - root5
                              - (32) red
                                -- (33) black
                     ==>        -- (34) yellow
                              - (35) green
                                -- (36) blue
                              - (37) brown