marshmallow / laravel-attributes
Marshmallow Attributes 是一个强大、智能且集成的实体-属性-值模型(EAV)实现,为 Laravel Eloquent 提供,可以轻松地将实体属性作为关系管理。它利用 Laravel Eloquent 的强大功能,实现平滑无缝的集成。
Requires
- php: ^8.0|^8.1
- jeremeamia/superclosure: ^2.4.0
- marshmallow/translatable: ^v1.20.0
- spatie/eloquent-sortable: ^4.0
- spatie/laravel-sluggable: ^3.3.0
- watson/validating: ^7.0.0
Requires (Dev)
- codedungeon/phpunit-result-printer: ^0.31.0
- orchestra/testbench: ^v6.24.0
- phpunit/phpunit: ^9.5.12
README
Marshmallow Attributes 是一个强大、智能且集成的实体-属性-值模型(EAV)实现,为 Laravel Eloquent 提供,可以轻松地将实体属性作为关系管理。它利用 Laravel Eloquent 的强大功能,实现平滑无缝的集成。
致谢说明
此包是基于 IsraelOrtuno 的出色 EAV 包 重写的分支,原始荣誉归他所有。它已被广泛重写,核心概念相同,我们认为它从根本上来说是好的。这个分支的主要差异包括
- 使用 marshmallow/laravel-cacheable 大幅提高性能
- 使用它序列化和反序列化实体及其关系
- 与 Laravel 集成,没有框架无关的复杂性
- 属性可以通过枢纽表附加到零个、一个或多个实体
- 属性可以排序、生成缩略语、翻译、分组,最令人兴奋的是缓存
- 实体属性以尽可能自然的 Eloquent 方式被当作普通 属性 处理
- 实体属性也以尽可能自然的 Eloquent 方式被当作普通 关系 处理
目录
介绍
基础
序言
EAV 定义来自 Wikipedia
实体-属性-值模型(EAV)是一种数据模型,以高效的方式编码实体,其中可以用来描述实体的属性(属性、参数)数量可能非常大,但实际上应用于特定实体的数量相对较小。
实体
实体代表一个需要动态扩展其属性的实拟模型。例如:Product
、Customer
或 Company
模型很可能是实体。
在这种情况下,实体将由一个 Eloquent 模型表示。
属性
属性作为我们想要添加到实体中的“列”。一个属性将获得一个类似于 price
、cities
或 colors
的缩略语,以进行标识,并将附加到实体上。它还将非常紧密地与数据类型实例配合,在写入或从数据库读取时对其进行转换或格式化。
此属性还将负责定义一些默认行为,如数据验证或默认值。
值
这负责存储与特定属性和特定实体实例(行)相关联的数据值。
在 Marshmallow Attributes 实现中,一个 Value 实例将表示与特定实体实例相关的属性内容。
根据数据类型,值存储在不同的表中。字符串值将存储在默认名为 attribute_varchar_values
的表中,而整数值将使用 attribute_integer_values
,依此类推。这两个表的列相同,除了 content
列的数据类型,它根据存储的数据类型进行了适配。
性能损失
EAV 模型以其性能不足而闻名。与查询其他任何水平结构相比,它在查询数据方面的复杂性也很大。这种范式在许多文章中被标记为反模式,并且有很多关于是否应该使用它的争论。
由于我们将在不同的表中存储实体、属性和值,因此执行任何操作都需要进行多次查询。这意味着如果我们为一个实体注册了4个属性,那么该包将至少执行5次查询。
select * from `companies` select * from `attribute_varchar_values` where `attribute_id` = '1' and `attribute_varchar_values`.`entity_id` in ('1', '2', '3', '4', '5') and `eav_attribute_varchar_values`.`entity_type` = 'App\Models\Company' select * from `attribute_varchar_values` where `attribute_id` = '2' and `attribute_varchar_values`.`entity_id` in ('1', '2', '3', '4', '5') and `eav_attribute_varchar_values`.`entity_type` = 'App\Models\Company' select * from `attribute_varchar_values` where `attribute_id` = '3' and `attribute_varchar_values`.`entity_id` in ('1', '2', '3', '4', '5') and `eav_attribute_varchar_values`.`entity_type` = 'App\Models\Company' select * from `attribute_varchar_values` where `attribute_id` = '4' and `attribute_varchar_values`.`entity_id` in ('1', '2', '3', '4', '5') and `eav_attribute_varchar_values`.`entity_type` = 'App\Models\Company'
但是,有好消息! Marshmallow Attributes 利用 Marshmallow Cacheable,它透明地缓存模型结果,可能将这些查询减少到只有一个或甚至零个查询!是的,这是可能的,并且已经默认实现!!
获得的灵活性
然而,尽管存在性能问题,EAV 提供了非常高的灵活性。它允许我们具有动态属性,可以在任何时间添加/删除,而不会影响数据库结构。当与主要存储 NULL
值的列一起工作时也很有帮助。
考虑到您接受了 EAV 带来的性能不足,该包是在灵活性方面开发的,因此至少可以解决这个性能问题。可以通过在单个查询中加载所有与实体相关的值,并让一些 PHP 逻辑将它们组织成关系来提高性能,但最终决定不这样做,以使数据库查询更加灵活。
如以下所述,此包将实体值加载得就像它们是自定义 Eloquent 关系一样。这就是为什么我们可以像查询常规 Eloquent 关系一样轻松地查询它们。
将值作为关系加载将使我们能够只加载可能需要用于特定情况的值,而让其他一些值保持未加载。它还将使我们能够利用强大的 Eloquent 查询关系工具,以便我们可以根据将直接应用于值内容的条件轻松过滤从数据库检索的实体。
更多技术细节
Marshmallow\Attributes\Traits\Attributable
这个特质是最重要的,让其他类协同工作。
它负责处理实体内的交互。这个特质执行 EAV 属性的 set
和 get
操作,调用 RelationBuilder
类,将关系方法添加到 $entityAttributeRelations
数组中。这些关系可以像往常一样调用,因为我们正在重写 __call
魔法方法以寻找这些调用。它负责设置保存和删除的事件监听器,添加全局范围,并检索与该实体相关的属性。
尝试访问实体属性时,如果它对应于 EAV 属性,则此特质包含提供其值、创建新的值实例、更新集合或任何其他设置/获取交互的逻辑。
在读取值时,没有太多事情要检查,如果值存在,我们只需格式化并提供它,否则我们将返回 null 或空集合。
在设置值时,事情变得稍微复杂一些。在设置值时,目前有 3 件事情需要考虑
- 设置一个不存在于数据库中的单个值,因此我们必须创建新的模型实例并将其与属性和实体相关联。
- 更新现有单值模型的(数据库行)内容。
- 用新集合替换现有(或空)的值集合,因此我们必须删除之前存储的值(从数据库中删除)。
它还覆盖了一些实体方法,如 bootIfNotBooted
、relationsToArray
、setRelation
、getRelationValue
,以提供与 Eloquent 模型在所有可能方式下的流畅和无缝集成。它将一切连接起来。
// To build entity relations for every instance bootIfNotBooted(); // To include attributes as relations when converting to array/json relationsToArray(); // To link entity & attribute to value collections (multi-valued attributes) setRelation() // To let Eloquent use our attribute relations as part of the model getRelationValue()
Marshmallow\Attributes\Support\RelationBuilder
此类根据它们的类型创建 Eloquent 与属性值的关联。如果它们是多值,它将提供一个 hasMany
关联,否则只是一个 hasOne
。此类创建返回此类关联的闭包,可以直接从实体模型中调用。这些闭包存储在 \Marshmallow\Attributes\Traits\Attributable
特性中的 $entityAttributeRelations
属性中。
安装
-
通过 composer 安装此包
composer require marshmallow/laravel-attributes
-
发布资源(迁移和配置文件)
php artisan marshmallow-attributes:install
-
完成!
用法
将 EAV 添加到 eloquent 模型
Marshmallow Attributes 是专门为 Eloquent 设计的,对简单性的重视程度与 Laravel 相关的任何其他方面一样。要向 Eloquent 模型添加 EAV 功能,只需使用如下 \Marshmallow\Attributes\Traits\Attributable
特性
class Company extends Model { use \Marshmallow\Attributes\Traits\Attributable; }
就是这样,我们只需在 Eloquent 模型中包含该特性即可!
核心类型
\Marshmallow\Attributes\Models\Type\Text::class \Marshmallow\Attributes\Models\Type\Boolean::class \Marshmallow\Attributes\Models\Type\Integer::class \Marshmallow\Attributes\Models\Type\Varchar::class \Marshmallow\Attributes\Models\Type\Datetime::class
注册你的类型
Marshmallow Attributes 默认不注册任何类型,因为这被认为是实现细节,所以您需要注册上述核心类型,或扩展它们并仅注册自定义类型。
use Marshmallow\Attributes\Models\Attribute; Attribute::typeMap([ 'varchar' => Marshmallow\Attributes\Models\Type\Varchar::class, // ... 'custom' => \Path\To\Your\Type::class, ]);
注意:虽然您可以在应用程序的任何位置注册自定义类型,但建议在服务提供者的
boot
方法中这样做。
注册你的实体
// Push your entity fully qualified namespace app('marshmallow-attributes.entities')->push(\Path\To\Your\Entity::class); // Or push the morph class alias if any app('marshmallow-attributes.entities')->push('entity');
您可以在应用程序的任何位置调用 'marshmallow-attributes.entities'
服务,在任何请求生命周期中(建议在服务提供者的 boot
方法中)。它是一个单例对象,包含一个纯 Laravel Collection。
创建新属性
像任何正常的 Eloquent 模型一样,您可以创建属性如下
app('marshmallow-attributes.attribute')->create([ 'slug' => 'size', 'type' => 'varchar', 'name' => 'Product Size', 'entities' => ['App\Models\Company', 'App\Models\Product'], ]);
管理属性实体
每次您需要获取附加到特定属性上的实体时,您可以这样做
$attribute = app('marshmallow-attributes.attribute')->find(1); // Get attribute entities collection $attribute->entities // Get attribute entities query builder $attribute->entities(); // Delete attached attribute entities $attribute->entities()->delete(); // Attach attribute entities $attribute->entities()->createMany([ [...], [...], [...], ]); // Alternative way of attaching attribute entities $attribute->fill([ 'entities' => ['App\Models\Company', 'App\Models\Product'], ])->save(); // Get all attribute values of type varchar $values = $attribute->values('varchar')->get();
分配值
您可以将新创建的自定义属性像正常属性一样处理,是的!所有属性都是平等的!! 😄
您需要证明吗?好吧,看看以下示例,其中我们假设 price
是我们刚刚创建并链接到我们的 \App\Models\Product
模型的自定义属性
// Single value assignment $product = \App\Models\Product::find(1); $product->price = 123; $product->save(); // Mass assignment $product = \App\Models\Product::find(1); $product->fill(['price' => 123])->save();
是的,就是这样。很简单!您可以用自定义属性像处理正常属性一样,没有区别。关于 Eloquent 的所有好东西都适用于这里,无论您是更新单个字段、批量赋值、创建还是更新,它就是按预期工作!
Marshmallow\Attributes\Support\ValueCollection
Marshmallow Attributes 允许您注册多值属性。为了使处理集合更简单,我们包括了一个新的集合类型,它仅扩展了 Illuminate\Database\Eloquent\Collection
并提供了一些额外的功能。此类让我们能够向属性添加和删除值。它基本上是让用户玩一个集合类,而不必担心创建 Value 模型实例。一些代码将在这里有所帮助
// This is how it works $entity->cities->add('Alexandria'); // And this is what you would have to do without this collection: $value = new Varchar(['content' => 'Alexandria', 'attribute_id' => 1, 'entity_type' => 'App\Models\Company', 'entity_id' => 1]); $entity->cities->push($value); // You could also pass an array $entity->cities->add(['Alexandria', 'Cairo']);
集合可能会得到改进并添加更多功能,但就目前而言已经足够了。基于值的模型替换了 Eloquent 方法 newCollection
,以便在处理多值属性时返回此类集合。
查询模型
Marshmallow Attributes 尝试以 Eloquent 通常的方式完成所有工作。当加载模型时,它内部为每个实体属性创建一个常规关系。这意味着我们可以根据注册的属性值进行查询过滤,就像我们通常在查询 Eloquent 关系时做的那样
// Cities is an entity attribute $companies = Company::whereHas('Cities', function (\Illuminate\Database\Eloquent\Builder $builder) { $builder->where('content', 'Alexandria'); })->get();
或者简单地使用内置查询作用域如下
$companies = Company::hasAttribute('Cities', 'Alexandria')->get();
当然,您可以像处理正常 Eloquent 属性一样检索实体属性,或作为原始关系
$company = Company::find(1); // Get entity attributes $company->cities; // Get entity raw relation $company->cities();
预加载
Marshmallow 属性 考虑到强大的 Eloquent 懒加载系统。在访问 Eloquent 模型中的实体属性时,它将与 Eloquent 处理关系时一样,在需要时才加载。然而,我们可以使用 Eloquent 懒加载来更好地处理 Marshmallow 属性,以提高性能并避免 n+1 查询问题。
Marshmallow 属性 为加载所有已注册的属性保留了一个特殊的关系名称。这个关系称为 eav
。当使用 eav
加载值时,它将加载与我们所操作的实体相关的所有属性,就像你显式地包括所有关系在 $with
模型属性中一样。
懒加载
同样,我们可以决定何时加载我们的属性,就像通常加载一个关系一样。
$company->load('eav'); $company->load('cities', 'colors');
使用 $with 自动加载
Eloquent 内置了一个 $with
,它接受一个关系数组,这些关系应该被懒加载。我们也可以使用它。
namespace App\Models; use Marshmallow\Attributes\Traits\Attributable; class Company extends Model { use Attributable; // Eager loading all the registered attributes protected $with = ['eav']; // Or just load a few of them protected $with = ['cities', 'colors']; }
注意:如果你的模型懒加载了
eav
关系,并且它已经被排队用于发送通知,这可能会导致一些问题,因为eav
关系是通过全局作用域评估的,而用于排队通知的SerializesAndRestoresModelIdentifiers
特性在反序列化排队模型时没有全局作用域,所以你会得到 "Call to undefined relationship [eav] on model [App\Models\Company]" 异常。
变更日志
请参阅 变更日志 了解项目的完整历史。
支持
以下支持渠道可供您选择
贡献 & 协议
感谢您考虑为这个项目做出贡献!贡献指南可以在 CONTRIBUTING.md 中找到。
欢迎提交错误报告、功能请求和拉取请求。
安全漏洞
如果您在这个项目中发现了安全漏洞,请发送电子邮件至 help@marshmallow.com。所有安全漏洞都将得到及时处理。
关于 Marshmallow
Marshmallow 是一家成立于2016年6月的开罗,埃及的软件解决方案初创公司,专注于为中小企业提供集成企业解决方案。我们相信,我们的动力——价值、影响力和影响力——是我们与众不同的地方,并通过软件的力量激发我们哲学的无限可能性。我们喜欢称之为“生活节奏的创新”。这就是我们如何为推进人类文明做出贡献。
许可证
本软件在 MIT 许可证 (MIT) 下发布。
(c) 2016-2021 Marshmallow LLC,部分权利保留。