viklr /materialized-model
Eloquent模型层次结构的Materialized Path
Requires
- php: ^8.1
- illuminate/database: ^8.0|^9.0|^10.0
- illuminate/support: ^8.0|^9.0|^10.0
Requires (Dev)
- mockery/mockery: ^1.4
- orchestra/testbench: ^6.0|^7.0|^8.0
- phpunit/phpunit: ^8.5|^9.5|^10.0
README
这个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\HasMaterializedPaths或Vicklr\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与您的模型一起使用。以下是一些示例。
- 创建根节点
- 插入节点
- 删除节点
- 移动节点
- 向节点提问
- 关系
- 根作用域
- 访问祖先/后代链
- 限制返回子节点的级别
- 自定义排序列
- 树层次结构
- 模型事件:
MaterializedModelMovedEvent - 软删除
- 杂项/实用函数
创建根节点
默认情况下,所有节点都创建为根节点
$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关系:parent和children。
$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();
同样,您可以通过提供所需的深度限制作为第一个参数,使用getDescendants和getDescendantsAndSelf方法来限制子代层级
// 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()方法重建(或重新计算)模型及其子节点的路径。
此方法将重新索引所有您的path和depth列值,仅从父<->子关系角度检查您的树。这意味着您只需要一个正确填充的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发送电子邮件,而不是使用问题跟踪器。
鸣谢
- Estanislau Trepat为其Baum包
- 感谢 Freek Van der Herten 和 Spatie 为文档提供的灵感。
许可协议
MIT 许可协议 (MIT)。请参阅 许可文件 获取更多信息。