viklr/materialized-model

Eloquent模型层次结构的Materialized Path

2.1 2024-02-05 09:10 UTC

This package is auto-updated.

Last update: 2024-09-05 10:34:09 UTC


README

Latest Stable Version MIT Licensed GitHub Tests Action Status Total Downloads

这个Laravel包为您的模型添加了层次结构功能。

Materialized Model

Materized Model是Laravel的Eloquent ORM中实现Materialized Paths模式的实例。

文档

关于Materialized Paths

Materialized Paths模式是一种拥有节点树层次结构的方法,除了节点数据外,它还存储节点的父节点ID或路径作为字符串。虽然Materialized Paths模式需要处理字符串和正则表达式的额外步骤,但它也提供了更多处理路径的灵活性,例如通过部分路径查找节点。

例如,您可以在单个查询中获取节点的所有后代,无论树有多深。缺点是插入/移动/删除需要额外的操作,但这由这个包在幕后处理。

Materialized Paths适用于有序树(例如菜单、商业类别、文件夹结构)以及必须高效查询的大树(例如线程化帖子)。

安装

Materialized Model与Laravel 8及以后版本兼容。您可以通过以下命令将其添加到项目中:

composer require vicklr/materialized-model

入门指南

包正确安装后,就可以应用到您的模型上了。

  • 将Vicklr/MaterializedModel/Traits/HasMaterializedPaths特性添加到扩展Illuminate\Database\Eloquent\Model的类中
  • 或者如果需要修改列名,则扩展Vicklr/MaterializedModel/MaterializedModel类
  • 或者将Vicklr/MaterializedModel/Traits/HasOrderedMaterializedPaths特性添加到扩展Illuminate\Database\Eloquent\Model的类中,该类应自动按数字排序字段排序

模型配置

为了与Materialized Model一起工作,您必须确保您的模型类使用Vicklr\MaterializedModel\Traits\HasMaterializedPathsVicklr\MaterializedModel\Traits\HasOrderedMaterializedPaths

这是最简单的方法

use Vicklr\MaterializedModel\Traits\HasMaterializedPaths;
use Illuminate\Database\Eloquent\Model;

class Category extends Model 
{
  use HasMaterializedPaths;
}

这是一个稍微复杂一些的例子,其中列名已自定义。为此,我们需要从使用特质的基类继承 - 包中包含这样一个基类,但您也可以提供自己的,只要它使用上述描述的特性即可

use Vicklr\MaterializedModel\MaterializedModel;
class Dictionary extends MaterializedModel 
{
  protected $table = 'dictionary';

  // 'parent_id' column name
  protected string $parentColumn = 'parent_id';

  // 'depth' column name
  protected string $depthColumn = 'depth';
  
  // 'path' column name
  protected string $pathColumn = 'path';
  
  // 'order' column name
  protected string $orderColumn = 'weight';

  // guard attributes from mass-assignment
  protected $guarded = array('id', 'parent_id', 'depth', 'path', 'weight');

}

请记住,显然,列名必须与数据库表中的列名匹配。

迁移配置

您必须确保支持您的Materialized Models的数据库表具有以下列

  • parent_id:对父节点的引用(int)
  • depth:深度或嵌套级别(int)
  • path:祖先路径(string)
  • ordering:排序顺序(string或int)

为此,我们在Blueprint上有两个辅助宏:materializedFields()和materializedOrdering()

materializedFields()辅助宏将设置必要的字段以建立层次结构,而materializedOrdering添加了数字排序字段。如果您不想使用数字排序,则可以自己定义该字段,并记得将其设置为模型上的orderColumn

以下是一个示例迁移文件

class CreateCategoriesTable extends Migration {

  public function up() {
    Schema::create('categories', function(Blueprint $table) {
      $table->id();

      $table->materializedFields(parent_name: 'parent_id', path_name: 'path', depth_name: 'depth', primary_name: 'id');
      $table->materializedOrdering(order_name: 'weight');
    });
  }

  public function down() {
    Schema::drop('categories');
  }

}

您可以自由修改列名,但请记住在模型中也更改它们。

使用

在配置好模型并运行迁移后,您现在可以使用MaterializedModel与您的模型一起使用。以下是一些示例。

创建根节点

默认情况下,所有节点都创建为根节点

$root = Category::create(['name' => 'Root category']);

或者,您可能需要将现有的节点转换为根节点

$node->makeRoot();

插入节点

// Directly with a relation
$child1 = $root->children()->create(['name' => 'Child 1']);

// with the `makeChildOf` method
$child2 = Category::create(['name' => 'Child 2']);
$child2->makeChildOf($root);

删除节点

$child1->delete();

由于数据库中的外键约束,删除节点的后代也会被删除。请注意,目前对于后代的删除已删除模型事件将不会触发。

移动节点

Materialized Model提供了几种移动节点的方法

  • makeNextSiblingOf($otherNode):使节点成为……的下一个兄弟节点
  • makePreviousSiblingOf($otherNode):使节点成为……的前一个兄弟节点
  • makeSiblingOf($otherNode):makeNextSiblingOf()的别名
  • makeChildOf($otherNode):使节点成为……的子节点
  • makeRoot():使当前节点成为根节点

例如

$root = Creatures::create(['name' => 'The Root of All Evil']);

$dragons = Creatures::create(['name' => 'Here Be Dragons']);
$dragons->makeChildOf($root);

$monsters = new Creatures(['name' => 'Horrible Monsters']);
$monsters->save();

$monsters->makeSiblingOf($dragons);

向节点提问

您可以向Materialized Model节点提出一些问题

  • isRoot():如果这是根节点,则返回true。
  • isChild():如果这是子节点,则返回true。
  • isDescendantOf($other):如果节点是其他节点的后代,则返回true。
  • isSelfOrDescendantOf($other):如果节点是自身或后代,则返回true。
  • isAncestorOf($other):如果节点是其他节点的祖先,则返回true。
  • isSelfOrAncestorOf($other):如果节点是自身或祖先,则返回true。

使用前面的示例中的节点

$demons->isRoot(); // => false

$demons->isDescendantOf($root); // => true

关系

Materialized Model为您的节点提供了两个自引用的Eloquent关系:parentchildren

$parent = $node->parent()->get();

$children = $node->children()->get();

根作用域

Materialized Model提供了一些基本的查询作用域,用于访问根节点

// Query scope which targets all root nodes
Category::roots();

您可能还只对第一个根节点感兴趣

$firstRootNode = Category::root();

访问祖先/后代链

Materialized Model提供了几种方法来访问树中节点的祖先/后代链。要记住的主要事情是它们以两种方式提供

首先作为查询作用域,返回一个Illuminate\Database\Eloquent\Builder实例以继续查询。要从这些中获取实际结果,请记住调用get()first()

  • ancestorsAndSelf():针对包括当前节点在内的所有祖先链节点。
  • ancestors():查询不包括当前节点的祖先链节点。
  • siblingsAndSelf():实例作用域,针对包括自我在内的父节点的所有子节点。
  • siblings():针对父节点的所有子节点的作用域,不包括自我。
  • descendantsAndSelf():作用域针对自身及其所有嵌套子节点。
  • descendants():所有子节点和嵌套子节点集合。

其次,作为返回实际实例(在适当的Collection对象中)的方法

  • getRoot():返回从当前节点开始的根节点。
  • getAncestorsAndSelf():检索包括当前节点在内的所有祖先链。
  • getAncestors():从数据库中获取所有祖先链,不包括当前节点。
  • getSiblingsAndSelf():获取包括自我在内的父节点的所有子节点。
  • getSiblings():返回父节点的所有子节点,不包括自我。
  • getNextSibling():返回具有相同父节点并且在排序中下一个的兄弟节点(如果有)
  • getPreviousSibling():返回具有相同父节点并且在排序中位于当前节点之前(如果有)的兄弟节点
  • getDescendantsAndSelf():获取所有嵌套子节点和自身。
  • getDescendants():获取所有子节点及其嵌套子节点。

以下是一个简单的示例,用于迭代一个节点的子节点(假设存在一个name属性)

$node = Category::where('name', '=', 'Books')->first();

$node->getDescendantsAndSelf()->each(function($descendant) {
  echo "{$descendant->name}";
});

限制返回子节点的层级

在某些情况下,如果层次深度很大,可能需要限制返回子节点的层级数(深度)。您可以在Materialized Model中使用limitDepth查询作用域来实现。

以下代码片段将获取当前节点及其下方最多5层深度的子节点

$node->descendants()->limitDepth(5)->get();

同样,您可以通过提供所需的深度限制作为第一个参数,使用getDescendantsgetDescendantsAndSelf方法来限制子代层级

// This will work without depth limiting
// 1. As usual
$node->getDescendants();
// 2. Selecting only some attributes
$other->getDescendants(array('id', 'parent_id', 'name'));
...
// With depth limiting
// 1. A maximum of 5 levels of children will be returned
$node->getDescendants(5);
// 2. A max. of 5 levels of children will be returned selecting only some attrs
$other->getDescendants(5, array('id', 'parent_id', 'name'));

自定义排序列

在Materialized Model中,所有结果都按您在模型中指定的排序列排序

protected $orderColumn = 'name';

导出层次树

Materialized Model包含HierarchyCollection,它扩展了默认的Eloquent\Collection类,并提供了toHierarchy方法,该方法返回表示查询树的嵌套集合。

将完整的树层次结构检索到一个普通的Collection对象中,并正确嵌套其子节点,就像这样

$tree = Category::where('name', '=', 'Books')->first()->getDescendantsAndSelf()->toHierarchy();

在集合上执行树操作

Materialized Model的HierarchyCollection可以用节点集合实例化,并且可以对这些节点执行操作,只要在HierarchyCollection上设置了类名

$nodes = ... // Collection of nodes retrieved from the database, by ids or some other means
$ancestors = (new HierarchyCollection($nodes))->setClassName(Category::class)->getAncestors();
// $ancestors will now contain all ancestors of all the nodes in the collection

以下操作适用于HierarchyCollection

  • getAncestorsAndSelves():检索所有祖先,包括当前节点。
  • getAncestors():检索所有祖先。
  • getDescendantsAndSelves():检索所有嵌套子节点,包括当前节点。
  • getDescendants():检索所有子节点及其嵌套子节点。

模型事件:MaterializedModelMovedEvent

Materialized Model模型在模型在层次结构中移动时发送MaterializedModelMovedEvent。

此事件可以通过监听器进行处理,该监听器可以从事件中检索移动的模型及其适用的先前父级。

树重建

Materialized Model支持通过rebuild()方法重建(或重新计算)模型及其子节点的路径。

此方法将重新索引所有您的pathdepth列值,仅从父<->子关系角度检查您的树。这意味着您只需要一个正确填充的parent_id列,Materialized Model将尽力重新计算其余部分。

当索引值出现严重问题时,这非常有用,或者当转换到另一个实现(可能有一个parent_id列)时,这可能非常有用。

简单示例用法,给定一个Category节点类

Category::roots()->each->rebuild();

软删除

Materialized Model不特别处理软删除,尽管只要恢复的节点的父级不是软删除,它应该正常工作。

杂项/实用函数

节点提取查询作用域

Materialized Model提供了一些查询作用域,可以用来从当前结果集中提取(删除)选定的节点。

  • withoutNode(node):从当前结果集中排除指定的节点。
  • withoutNodes(nodes):从当前结果集中排除指定的节点集合。
  • withoutSelf():从当前结果集中排除自身。
$node = Category::where('name', '=', 'Some category I do not want to see.')->first();

$root = Category::where('name', '=', 'Old boooks')->first();
var_dump($root->descendantsAndSelf()->withoutNode($node)->get());
... // <- This result set will not contain $node

变更日志

请参阅CHANGELOG了解最近更改的更多信息。

贡献

请参阅CONTRIBUTING获取详细信息。

安全

如果您发现任何安全相关的问题,请通过security@vicklr.com发送电子邮件,而不是使用问题跟踪器。

鸣谢

许可协议

MIT 许可协议 (MIT)。请参阅 许可文件 获取更多信息。