khazhinov/laravel-trees

Laravel 多树结构


README

PHP Laravel Package Total Downloads License composer.lock available

Latest Stable Version

Maintainability Test Coverage

内容

信息

本包是多树结构(有很多根节点)。

table image

嵌套集是什么?

嵌套集或嵌套集模型是一种在关系表中有效地存储层次数据的方法。来自维基百科

嵌套集模型是按照树遍历的顺序编号节点,遍历每个节点两次,按遍历顺序分配数字,并在两次遍历中。这为每个节点留下两个数字,这两个数字作为两个属性存储。查询变得便宜:可以通过比较这些数字来测试层次结构成员资格。更新需要重新编号,因此很昂贵。

应用

当树很少更新时,NSM 显示出良好的性能。它被调整以快速获取相关节点。它非常适合构建多级菜单或商店分类。

要求

  • PHP: ^8.0
  • Laravel: ^8.80 | ^9.2

强烈建议使用支持事务的数据库(如MySQL的InnoDB,Postgres)来确保树不会受到可能的损坏。

安装

要安装此包,请在终端中

composer require efureev/laravel-trees

测试

./vendor/bin/phpunit --testdox

composer test

文档

此包支持不同的模型主键:intuuid。此包允许创建多根结构:不仅只有一个根!还可以在树之间移动节点。

迁移

单树结构模型

<?php
namespace App\Models;

use Fureev\Trees\NestedSetTrait;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    use NestedSetTrait;

}

或使用自定义基本配置

<?php
namespace App\Models;

use Fureev\Trees\{NestedSetTrait,Contracts\TreeConfigurable};
use Fureev\Trees\Config\Base;
use Illuminate\Database\Eloquent\Model;

class Category extends Model implements TreeConfigurable
{
    use NestedSetTrait;

    protected static function buildTreeConfig(): Base
    {
        return new Base();
    } 
}

或使用自定义配置

    protected static function buildTreeConfig(): Base
    {
        return Base::make()
            ->setAttribute('parent', ParentAttribute::make()->setName('papa_id'))
            ->setAttribute('left', LeftAttribute::make()->setName('left_offset'))
            ->setAttribute('right', RightAttribute::make()->setName('right_offset'))
            ->setAttribute('level', LevelAttribute::make()->setName('deeeeep'));
    }

多树结构模型和主键类型 uuid

<?php
namespace App\Models;

// use Fureev\Trees\Config\TreeAttribute;
use Fureev\Trees\Contracts\TreeConfigurable;
use Fureev\Trees\NestedSetTrait;
use Fureev\Trees\Config\Base;
use Illuminate\Database\Eloquent\Model;

class Item extends Model implements TreeConfigurable
{
    use NestedSetTrait;
    
    protected $keyType = 'uuid';

    protected static function buildTreeConfig(): Base
    {
        $config= new Base(true);
        // $config->parent()->setType('uuid'); <-- `parent type` set up automatically from `$model->keyType`

        return $config;
    }
    /*
    or:
     
    protected static function buildTreeConfig(): Base
    {
        return Base(TreeAttribute::make('uuid')->setAutoGenerate(false));
    }
    
    or:
     
    protected static function buildTreeConfig(): Base
    {
       return Base::make()
            ->setAttributeTree(TreeAttribute::make()->setName('big_tree_id'))
            ->setAttribute('parent', ParentAttribute::make()->setName('pid'))
            ->setAttribute('left', LeftAttribute::make()->setName('left_offset'))
            ->setAttribute('right', RightAttribute::make()->setName('right_offset'))
            ->setAttribute('level', LevelAttribute::make()->setName('deeeeep'));
    }
    */
}

在迁移中使用

<?php
use Fureev\Trees\Migrate;
use Illuminate\Database\Migrations\Migration;

class AddTemplates extends Migration
{
    public function up()
    {
        Schema::create('trees', function (Blueprint $table) {
            $table->uuid('id')->primary();
            $table->string('title');
    
            Migrate::columns($table, (new Page)->getTreeConfig());
    
            $table->timestamps();
            $table->softDeletes();
        });
    }
}

关系

节点具有以下关系,它们是完整功能性的,并且可以预加载

  • 节点属于 parent
  • 节点有多个 children
  • 节点有多个 ancestors
  • 节点有多个 descendantsNew

创建节点

创建根节点

当你创建一个根节点时:如果你使用 ...

  • 单模式:你可能只能创建一个根节点。
  • 多模式:它将作为根节点插入,并具有不同的 tree_id。默认:递增一次。你可以自定义此功能。

这些操作是相同的

// For single-root tree
Category::make($attributes)->makeRoot()->save(); 
Category::make($attributes)->saveAsRoot();
Category::create(['setRoot'=>true,...]);

// For multi-root tree. If parent is absent, node set as root.
Page::make($attributes)->save();

创建非根节点

当你创建一个非根节点时,它将被附加到父节点的末尾。

如果你想使节点成为其他节点的子节点,你可以将其作为最后一个或第一个子节点。

在以下示例中,$parent 是某个现有节点。

附加到指定的父节点

将子节点添加到节点中。在父节点的其他子节点之后插入。

$node->appendTo($parent)->save();
预先附加到指定的父节点

将子节点添加到节点中。在父节点的其他子节点之前插入。

$node->prependTo($parent)->save();
在父节点之前插入

将子节点添加到同一父节点。在目标节点之前插入。

$node->insertBefore($parent)->save();
在父节点之后插入

将子节点添加到同一父节点。在目标节点之后插入。

$node->insertAfter($parent)->save();

移动节点

在父节点范围内向上移动节点
$node->up();
在父节点范围内向下移动节点
$node->down();

删除节点

要删除节点

$node->delete();

重要!如果删除的节点有子节点,则它们将附加到被删除节点的父节点。这种行为可能被更改。

重要!节点必须以模型的形式进行删除!不要尝试使用如下查询删除它们

Category::where('id', '=', $id)->delete();

这将破坏树结构!

SoftDeletes 特性也得到了支持,包括在模型级别。

您还可以删除所有子节点

$node->deleteWithChildren();

检索节点

在某些情况下,我们将使用一个 $id 变量,它是目标节点的 ID。

祖先和后代

祖先构成了从节点到其父节点的链。这对于显示当前类别的面包屑很有用。

后代是指子树中的所有节点,即节点的子节点、子节点的子节点等。

祖先和后代都可以进行预加载。

这些是关系

  • ancestors:祖先关系
  • descendantsNew:后代关系
  • children:多对多关系
  • parent:属于关系
// Accessing ancestors
$node->ancestors;

// Accessing descendants
$node->descendantsNew;

// Accessing descendants
$node->children;

父节点

获取父节点

$node->parent;

父节点集合

$node->parents($level);

兄弟节点

兄弟节点是与同一父节点具有相同父节点的节点。

// Get all siblings of the node
$collection = $node->siblings()->get();

// Get siblings which are before the node
$collection = $node->prevSiblings()->get();

// Get siblings which are after the node
$collection = $node->nextSiblings()->get();

// Get a sibling that is immediately before the node
$prevNode = $node->prevSibling()->first();

// Get a sibling that is immediately after the node
$nextNode = $node->nextSibling()->first();
$prevNode = $node->prev()->first();
$nextNode = $node->next()->first();

节点查询

模型助手

控制台树

table image

Table::fromModel($root->refresh())->draw();
$collection = Structure::all();
Table::fromTree($collection->toTree())
    ->hideLevel()
    ->setExtraColumns(
        [
            'title'                         => 'Label',
            $root->leftAttribute()->name()  => 'Left',
            $root->rightAttribute()->name() => 'Right',
            $root->levelAttribute()->name() => 'Deep',
        ]
    )
    ->draw($output);
Structure::all()->toOutput([],null,'...');

检查一致性

您可以检查树是否损坏(即存在某些结构错误)

$bool = Category::isBroken();

可以获取错误统计信息

$data = Category::countErrors();

它将返回一个包含以下键的数组

  • oddness - 错误的 lftrgt 值的节点数量
  • duplicates - 拥有相同的 lftrgt 值的节点数量
  • wrong_parent - 拥有无效的 parent_id 值的节点数量,该值不对应于 lftrgt
  • missing_parent - 拥有 parent_id 指向不存在节点的节点数量

修复树

从 v3.3.1 版本开始,现在可以修复树。

对于单个树

Node::fixTree();

对于多树

Node::fixMultiTree();