allcaretravel / laravel-categories
Rinvex Categories 是一个用于分类管理的多态 Laravel 扩展包。您可以使用它轻松地对任何 Eloquent 模型进行分类,并利用嵌套集合、Sluggable 和可翻译模型的力量。
Requires
- php: ^7.4.0
- allcaretravel/laravel-support: ^5.0
- illuminate/console: ^8.0
- illuminate/database: ^8.0
- illuminate/support: ^8.0
- kalnoy/nestedset: ^5.0
- spatie/laravel-sluggable: ^2.5
- spatie/laravel-translatable: ^4.5
Requires (Dev)
- codedungeon/phpunit-result-printer: ^0.27.0
- illuminate/container: ^8.0
- phpunit/phpunit: ^9.0.0
README
Rinvex Categories 是一个用于分类管理的多态 Laravel 扩展包。您可以使用它轻松地对任何 Eloquent 模型进行分类,并利用 嵌套集合、Sluggable 和 可翻译 模型的力量。
安装
-
通过 composer 安装该包
composer require rinvex/laravel-categories
-
发布资源(迁移和配置文件)
php artisan rinvex:publish:categories
-
执行以下命令以执行迁移
php artisan rinvex:migrate:categories
-
完成!
使用方法
要为您的 Eloquent 模型添加分类支持,只需使用 \Rinvex\Categories\Traits\Categorizable
特性。
管理您的分类
您的分类只是普通的 Eloquent 模型,因此您可以像处理它们一样处理它们。这里没有特别之处!
注意:由于 Rinvex Categories 扩展并利用了其他优秀的包,因此请查阅以下文档以获取更多详细信息:
- 使用
kalnoy/nestedset
强大的嵌套集合- 使用
spatie/laravel-sluggable
自动生成缩略名- 使用
spatie/laravel-translatable
默认可翻译
管理您的可分类模型
API 界面直观且非常直接,让我们快速浏览一下。
// Get all categories $allCategories = app('rinvex.categories.category')->first(); // Get instance of your model $post = new \App\Models\Post::find(); // Get attached categories collection $post->categories; // Get attached categories query builder $post->categories();
您可以通过多种方式附加分类
// Single category id $post->attachCategories(1); // Multiple category IDs array $post->attachCategories([1, 2, 5]); // Multiple category IDs collection $post->attachCategories(collect([1, 2, 5])); // Single category model instance $categoryInstance = app('rinvex.categories.category')->first(); $post->attachCategories($categoryInstance); // Single category slug $post->attachCategories('test-category'); // Multiple category slugs array $post->attachCategories(['first-category', 'second-category']); // Multiple category slugs collection $post->attachCategories(collect(['first-category', 'second-category'])); // Multiple category model instances $categoryInstances = app('rinvex.categories.category')->whereIn('id', [1, 2, 5])->get(); $post->attachCategories($categoryInstances);
注意
attachCategories()
方法将给定的分类附加到模型,而不会影响当前附加的分类。而syncCategories()
方法可以移除不在给定项目中的任何记录,此方法接受一个可选的布尔参数,该参数将分离标志设置为true
或false
。- 要移除模型分类,您可以使用
detachCategories()
方法,该方法与attachCategories()
方法具有完全相同的签名,并且具有附加功能,可以通过传递 null 或空值来移除所有当前附加的分类:$post->detachCategories();
。
如您所预期的那样,您还可以检查是否已附加分类
// Single category id $post->hasAnyCategories(1); // Multiple category IDs array $post->hasAnyCategories([1, 2, 5]); // Multiple category IDs collection $post->hasAnyCategories(collect([1, 2, 5])); // Single category model instance $categoryInstance = app('rinvex.categories.category')->first(); $post->hasAnyCategories($categoryInstance); // Single category slug $post->hasAnyCategories('test-category'); // Multiple category slugs array $post->hasAnyCategories(['first-category', 'second-category']); // Multiple category slugs collection $post->hasAnyCategories(collect(['first-category', 'second-category'])); // Multiple category model instances $categoryInstances = app('rinvex.categories.category')->whereIn('id', [1, 2, 5])->get(); $post->hasAnyCategories($categoryInstances);
注意
hasAnyCategories()
方法检查模型是否已附加了任何给定的分类。它返回布尔值true
或false
。- 类似地,
hasAllCategories()
方法与hasAnyCategories()
方法具有完全相同的签名,但它执行不同的操作,并执行严格的比较以检查是否已附加了所有给定的分类。
高级用法
生成分类缩略名
Rinvex Categories 自动生成缩略名,并在未提供时自动检测并插入默认翻译。但您仍然可以通过常规 Eloquent create
方法显式传递它,如下所示
app('rinvex.categories.category')->create(['name' => ['en' => 'My New Category'], 'slug' => 'custom-category-slug']);
注意:请参阅 Sluggable 包以获取更多详细信息。
智能参数检测
瑞文克斯分类 方法能够智能地处理列表中的分类,几乎可以处理所有类型的输入,如上例所示。它会检查输入类型并相应地处理。
检索所有与分类关联的模型
你可能需要获取特定分类的所有模型,你可以轻松地按照以下步骤进行
$category = app('rinvex.categories.category')->find(1); $category->entries(\App\Models\Post::class)->get();
查询范围
是的,瑞文克斯分类 为您提供了一些方便的查询范围,使用示例
// Single category id $post->withAnyCategories(1)->get(); // Multiple category IDs array $post->withAnyCategories([1, 2, 5])->get(); // Multiple category IDs collection $post->withAnyCategories(collect([1, 2, 5]))->get(); // Single category model instance $categoryInstance = app('rinvex.categories.category')->first(); $post->withAnyCategories($categoryInstance)->get(); // Single category slug $post->withAnyCategories('test-category')->get(); // Multiple category slugs array $post->withAnyCategories(['first-category', 'second-category'])->get(); // Multiple category slugs collection $post->withAnyCategories(collect(['first-category', 'second-category']))->get(); // Multiple category model instances $categoryInstances = app('rinvex.categories.category')->whereIn('id', [1, 2, 5])->get(); $post->withAnyCategories($categoryInstances)->get();
注意
withAnyCategories()
范围可以找到具有给定任何关联分类的文章。它通常返回查询构建器,因此您可以将其链接或调用get()
方法,例如,以执行并获取结果。- 类似地,还有一些其他范围,如
withAllCategories()
,可以找到具有给定所有关联分类的文章,withoutCategories()
可以找到没有给定任何关联分类的文章,最后是withoutAnyCategories()
,它找到没有任何关联分类的文章。所有范围都是平等创建的,具有相同的签名,并返回查询构建器。
分类翻译
按照以下方式轻松管理分类翻译
$category = app('rinvex.categories.category')->find(1); // Update title translations $category->setTranslation('name', 'en', 'New English Category Title')->save(); // Alternatively you can use default eloquent update $category->update([ 'name' => [ 'en' => 'New Category', 'ar' => 'تصنيف جديد', ], ]); // Get single category translation $category->getTranslation('name', 'en'); // Get all category translations $category->getTranslations('name'); // Get category title in default locale $category->name;
注意:有关详细信息,请查看 可翻译 包。
管理您的节点/嵌套集
插入分类
移动和插入分类包括多个数据库查询,因此当分类保存时,将自动开始 事务。如果您正在处理多个模型,则可以安全地使用全局事务。
另一个重要注意事项是,结构操作被延迟,直到您在模型上调用 save
(一些方法隐式调用 save
并返回操作的真值结果)。
如果模型成功保存,并不意味着分类已移动。如果您的应用程序依赖于分类是否实际改变了位置,请使用 hasMoved
方法
if ($category->save()) { $moved = $category->hasMoved(); }
创建分类
当您简单地创建一个分类时,它将被附加到树的末尾
app('rinvex.categories.category')->create($attributes); // Saved as root $category = app('rinvex.categories.category')->fill($attributes); $category->save(); // Saved as root
在这种情况下,该分类被认为是 根,这意味着它没有父级。
从现有分类创建根
分类将被附加到树的末尾
// #1 Implicit save $category->saveAsRoot(); // #2 Explicit save $category->makeRoot()->save();
附加和前置到指定的父级
如果您想将分类作为其他分类的子分类,您可以将其设置为最后一个或第一个子分类。假设 $parent
是某个现有分类,以下是一些附加分类的方法
// #1 Using deferred insert $category->appendToNode($parent)->save(); // #2 Using parent category $parent->appendNode($category); // #3 Using parent's children relationship $parent->children()->create($attributes); // #5 Using category's parent relationship $category->parent()->associate($parent)->save(); // #6 Using the parent attribute $category->parent_id = $parent->getKey(); $category->save(); // #7 Using static method app('rinvex.categories.category')->create($attributes, $parent);
只有几种方法可以前置
// #1 Using deferred insert $category->prependToNode($parent)->save(); // #2 Using parent category $parent->prependNode($category);
在指定分类之前或之后插入
您可以将 $category
设置为与 $neighbor
分类相邻。假设 $neighbor
是某个现有分类,而目标分类可以是新的。如果目标分类存在,它将被移动到新位置,并且如果需要,父级将更改。
# Explicit save $category->afterNode($neighbor)->save(); $category->beforeNode($neighbor)->save(); # Implicit save $category->insertAfterNode($neighbor); $category->insertBeforeNode($neighbor);
从数组构建树
当在分类上使用静态方法 create
时,它会检查属性是否包含 children
键。如果包含,它将递归地创建更多分类,如下所示
$category = app('rinvex.categories.category')->create([ 'name' => [ 'en' => 'New Category Title', ], 'children' => [ [ 'name' => 'Bar', 'children' => [ [ 'name' => 'Baz' ], ], ], ], ]);
$category->children
现在包含已创建的子分类列表。
从数组重新构建树
您可以轻松地重新构建树。这对于大量更改树的结构非常有用。给定 $data
作为分类数组,您可以按照以下方式构建树
$data = [ [ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ], [ 'name' => 'bar' ], ]; app('rinvex.categories.category')->rebuildTree($data, $delete);
为标题为 foo
的类别指定了 ID,这意味着现有类别将被填充并保存。如果类别不存在,将抛出 ModelNotFoundException
。此外,此类别还指定了 children
,它也是一个类别数组;它们将以相同的方式处理并保存为 foo
类别的子类别。
类别 bar
没有指定主键,因此它将被视为新类别,并被创建。
$delete
表示是否删除在 $data
中不存在但已存在的类别。默认情况下,类别不会被删除。
检索分类
在某些情况下,我们将使用一个 $id
变量,它是目标类别的 ID。
祖先
祖先构成类别的父级链。这对于显示当前类别的面包屑很有用。
// #1 Using accessor $result = $category->getAncestors(); // #2 Using a query $result = $category->ancestors()->get(); // #3 Getting ancestors by primary key $result = app('rinvex.categories.category')->ancestorsOf($id);
后代
后代是指子树中的所有类别,即类别的子类别、子类别的子类别等。
// #1 Using relationship $result = $category->descendants; // #2 Using a query $result = $category->descendants()->get(); // #3 Getting descendants by primary key $result = app('rinvex.categories.category')->descendantsOf($id); // #3 Get descendants and the category by id $result = app('rinvex.categories.category')->descendantsAndSelf($id);
后代可以被预加载
$categories = app('rinvex.categories.category')->with('descendants')->whereIn('id', $idList)->get();
兄弟
兄弟是具有相同父级的类别。
$result = $category->getSiblings(); $result = $category->siblings()->get();
仅获取下一个兄弟
// Get a sibling that is immediately after the category $result = $category->getNextSibling(); // Get all siblings that are after the category $result = $category->getNextSiblings(); // Get all siblings using a query $result = $category->nextSiblings()->get();
获取上一个兄弟
// Get a sibling that is immediately before the category $result = $category->getPrevSibling(); // Get all siblings that are before the category $result = $category->getPrevSiblings(); // Get all siblings using a query $result = $category->prevSiblings()->get();
从其他表中获取相关模型
想象一下,每个类别 has many
产品。即 HasMany
关系已建立。您如何获取 $category
和其所有后代的全部产品?很简单!
// Get ids of descendants $categories = $category->descendants()->pluck('id'); // Include the id of category itself $categories[] = $category->getKey(); // Get products $goods = Product::whereIn('category_id', $categories)->get();
现在想象一下,每个类别 has many
帖子。即这次建立 morphToMany
关系。您如何获取 $category
和其所有后代的全部帖子?这可能吗?当然可以!
// Get ids of descendants $categories = $category->descendants()->pluck('id'); // Include the id of category itself $categories[] = $category->getKey(); // Get posts $posts = \App\Models\Post::withCategories($categories)->get();
包括分类深度
如果您需要知道类别处于哪个级别
$result = app('rinvex.categories.category')->withDepth()->find($id); $depth = $result->depth;
根类别将在级别 0。根类别的子类别将具有级别 1,等等。要获取指定级别的类别,您可以在查询构建器上应用 having
约束
$result = app('rinvex.categories.category')->withDepth()->having('depth', '=', 1)->get();
默认排序
每个类别都有其唯一的 _lft
值,该值决定了其在树中的位置。如果您想按此值对类别进行排序,您可以在查询构建器上使用 defaultOrder
方法
// All categories will now be ordered by lft value $result = app('rinvex.categories.category')->defaultOrder()->get();
您可以按相反的顺序获取类别
$result = app('rinvex.categories.category')->reversed()->get();
移动分类
为了在父类别内部上移或下移类别以影响默认顺序
$bool = $category->down(); $bool = $category->up(); // Shift category by 3 siblings $bool = $category->down(3);
操作的结果是布尔值,表示类别是否已更改其位置。
约束
可以应用于查询构建器的各种约束
- whereIsRoot() 获取仅根类别;
- whereIsAfter($id) 获取每个在指定 ID 类别之后(不仅是兄弟)的类别;
- whereIsBefore($id) 获取在指定 ID 类别之前的每个类别。
后代约束
$result = app('rinvex.categories.category')->whereDescendantOf($category)->get(); $result = app('rinvex.categories.category')->whereNotDescendantOf($category)->get(); $result = app('rinvex.categories.category')->orWhereDescendantOf($category)->get(); $result = app('rinvex.categories.category')->orWhereNotDescendantOf($category)->get(); // Include target category into result set $result = app('rinvex.categories.category')->whereDescendantOrSelf($category)->get();
祖先约束
$result = app('rinvex.categories.category')->whereAncestorOf($category)->get();
$category
可以是模型的 primary key 或模型实例。
构建树
在获取一组类别后,您可以将其转换为树。例如
$tree = app('rinvex.categories.category')->get()->toTree();
这将填充集合中每个类别的 parent
和 children
关系,您可以使用递归算法渲染树
$categories = app('rinvex.categories.category')->get()->toTree(); $traverse = function ($categories, $prefix = '-') use (&$traverse) { foreach ($categories as $category) { echo PHP_EOL.$prefix.' '.$category->name; $traverse($category->children, $prefix.'-'); } }; $traverse($categories);
这将输出以下内容
- Root
-- Child 1
--- Sub child 1
-- Child 2
- Another root
构建平面树
此外,您还可以构建一个平面树:一个类别列表,其中子类别直接跟在父类别后面。这在您获取具有自定义排序(例如按字母顺序)的类别且不想使用递归遍历类别时很有用。
$categories = app('rinvex.categories.category')->get()->toFlatTree();
获取子树
有时您不需要加载整个树,只需要加载特定类别的子树
$root = app('rinvex.categories.category')->find($rootId); $tree = $root->descendants->toTree($root);
现在 $tree
包含 $root
类别的子类别。
如果您不需要 $root
类别本身,可以这样做
$tree = app('rinvex.categories.category')->descendantsOf($rootId)->toTree($rootId);
删除分类
要删除类别
$category->delete();
重要!类别拥有的任何后代也将被 删除!
重要!必须将类别作为模型进行删除,不要尝试使用如下查询删除它们
app('rinvex.categories.category')->where('id', '=', $id)->delete();
这将破坏树!
辅助方法
// Check if category is a descendant of other category $bool = $category->isDescendantOf($parent); // Check whether the category is a root: $bool = $category->isRoot(); // Other checks $category->isChildOf($other); $category->isAncestorOf($other); $category->isSiblingOf($other);
检查一致性
您可以检查树是否已损坏(即存在某些结构错误)
// Check if tree is broken $bool = app('rinvex.categories.category')->isBroken(); // Get tree error statistics $data = app('rinvex.categories.category')->countErrors();
树错误统计将返回一个包含以下键的数组
oddness
-- 具有错误lft
和rgt
值的类别数量duplicates
-- 具有相同lft
或rgt
值的类别数量wrong_parent
-- 具有无效的parent_id
值(不对应lft
和rgt
值)的分类数量missing_parent
-- 指向不存在分类的parent_id
的分类数量
修复树
分类树现在可以修复(如果已损坏)。使用 parent_id
列的继承信息,为每个分类设置适当的所有 _lft
和 _rgt
值。
app('rinvex.categories.category')->fixTree();
注意:有关详细信息,请参阅 Nested Sets 包。
变更日志
请参阅 变更日志,了解项目的完整历史。
支持
以下支持渠道随时为您服务
贡献 & 协议
感谢您考虑为该项目做出贡献!贡献指南可在 CONTRIBUTING.md 中找到。
欢迎提交错误报告、功能请求和拉取请求。
安全漏洞
我们希望确保此包对每个人都安全。如果您在此包中发现安全漏洞,我们感谢您以 负责任的方式 向我们披露。
公开披露漏洞可能会使整个社区面临风险。如果您发现安全担忧,请通过电子邮件发送至 help@rinvex.com。我们将与您合作,确保我们理解问题的范围,并完全解决您的担忧。我们将视为 help@rinvex.com 的最高优先级,并尽快解决任何问题。
安全漏洞得到纠正后,将尽快部署安全热修复版本。
关于 Rinvex
Rinvex 是一家软件解决方案初创公司,自 2016 年 6 月在埃及亚历山大成立以来,专门为中小企业提供集成企业解决方案。我们相信,我们的动力——价值、范围和影响——是我们与众不同的地方,通过软件的力量释放我们哲学的无限可能性。我们喜欢称之为“生活的速度创新”。这就是我们如何为推动人类进步做出我们的一份贡献。
许可
此软件根据 MIT 许可证 (MIT) 发布。
(c) 2016-2020 Rinvex LLC,部分权利保留。