kolossal-io / laravel-multiplex
一个将版本化元数据附加到 Eloquent 模型的 Laravel 扩展包。
Requires
- php: ^8.0
- illuminate/contracts: ^9.0|^10.0|^11.0
Requires (Dev)
- larastan/larastan: ^2.0.1
- laravel/pint: ^1.0
- mattiasgeniar/phpunit-query-count-assertions: ^1.1
- mockery/mockery: ^1.6
- nunomaduro/collision: ^6.1|^7.0|^8.0
- orchestra/testbench: ^7.0|^8.0|^9.0
- pestphp/pest: ^1.1|^2.35
- pestphp/pest-plugin-laravel: ^1.1|^2.0
- phpstan/extension-installer: ^1.1
- phpstan/phpstan-deprecation-rules: ^1.0
- phpstan/phpstan-phpunit: ^1.0
This package is auto-updated.
Last update: 2024-09-12 08:00:29 UTC
README
一个将时间切片元数据附加到 Eloquent 模型的 Laravel 扩展包。
功能
Multiplex 允许您以方便的方式将时间切片元数据附加到 Eloquent 模型。
$post = \App\Models\Post::first(); // Set meta fluently for any key – `likes` is no column of `Post`. $post->likes = 24; // Or use the `setMeta` method. $post->setMeta('likes', 24); // You may also schedule changes, for example change the meta in 2 years: $post->setMetaAt('likes', 6000, '+2 years');
特性
- 元数据以版本形式保存:安排元数据更改、更改历史或检索特定时间点的元数据。
- 支持流畅语法:将您的模型元数据用作属性。
- 多态关系允许在不担心数据库模式的情况下向任何 Eloquent 模型添加元数据。
- 易于尝试:在不触摸或删除原始列的情况下,通过可变元数据扩展您模型的现有数据库列。
- 基于 Laravel-Metable 的类型转换系统可以存储和检索多种不同标量和对象类型的数据。
为什么还需要另一个元数据包?
主要区别在于 Multiplex 中的元数据有一个定义有效性的时间戳。这允许跟踪和计划更改。您可以在特定时间点检查模型上的所有元数据,Multiplex 默认只为您提供最新的。
由于 Multiplex 在多态表中存储元数据,它可以轻松地集成到现有项目中以扩展模型属性。即使不删除模型的相关表列也可以这样做:它们作为后备使用。
并且它很低调:如果您不喜欢它,只需 移除 HasMeta
特性,一切就会恢复正常。
目录
安装
您可以通过 composer 安装此包。
composer require kolossal-io/laravel-multiplex
发布迁移以创建存储元数据的 meta
表。
php artisan migrate
将 HasMeta
特性附加到需要附加元数据的任何 Eloquent 模型。
use Illuminate\Database\Eloquent\Model; use Kolossal\Multiplex\HasMeta; class Post extends Model { use HasMeta; }
附加元数据
默认情况下,您可以使用任何 key
附加元数据。您可以 限制可用的键。
$model->setMeta('foo', 'bar'); // or $model->foo = 'bar';
您还可以通过传递一个 array
来设置多个元值。
$model->setMeta([ 'hide' => true, 'color' => '#000', 'likes' => 24, ]);
所有元数据将在保存模型时自动存储。
$model->foo = 'bar'; $model->isMetaDirty(); // true $model->save(); $model->isMetaDirty(); // false
您还可以不保存元数据来保存模型。
$model->saveWithoutMeta(); $model->isMetaDirty(); // true $model->saveMeta();
您还可以重置尚未保存的元数据更改。
$model->resetMeta();
元数据可以立即存储,而无需等待父模型保存。
// Save the given meta value right now. $model->saveMeta('foo', 123.45); // Save only specific keys of the changed meta. $model->setMeta(['color' => '#fff', 'hide' => false]); $model->saveMeta('color'); $model->isMetaDirty('hide'); // true // Save multiple meta values at once. $model->saveMeta([ 'color' => '#fff', 'hide' => true, ]);
安排元数据
您可以为特定的发布日期保存元数据。
$user = Auth::user(); $user->saveMeta('favorite_band', 'The Mars Volta'); $user->saveMetaAt('favorite_band', 'Portishead', '+1 week'); // Changing taste in music: This will return `The Mars Volta` now but `Portishead` in a week. $user->favorite_band;
这样,您还可以更改历史数据。
$user->saveMetaAt('favorite_band', 'Arctic Monkeys', '-5 years'); $user->saveMetaAt('favorite_band', 'Tool', '-1 year'); // This will return `Tool` – which is true since this is indeed a good band. $user->favorite_band;
您还可以一次保存多个元数据记录。
$user->setMeta('favorite_color', 'blue'); $user->setMeta('favorite_band', 'Jane’s Addiction'); $user->saveMetaAt('+1 week'); // or $user->saveMetaAt([ 'favorite_color' => 'blue', 'favorite_band' => 'Jane’s Addiction', ], '+1 week');
元数据的存储方式
Multiplex 会将元数据存储在多态表中,并为您处理数据类型的序列化和反序列化。底层的多态 meta
表可能看起来像这样
相应的元值看起来像这样
$post = Post::find(1); $post->color; // string(4) "#000" $post->likes; // int(24) $post->hide; // bool(true) // In the year 2030 `$post->color` will be `#fff`.
检索元数据
您可以将元数据访问为模型上的属性。
$post->likes; // (int) 24 $post->color; // (string) '#000'
或者使用getMeta()
方法为不存在的元指定一个回退值。
$post->getMeta('likes', 0); // Use `0` as a fallback.
您还可以检索模型上的meta
关系。这将只检索每个key
已发布的最新值。
$post->saveMeta([ 'author' => 'Anthony Kiedis', 'color' => 'black', ]); $post->saveMetaAt('author', 'Jimi Hendrix', '1970-01-01'); $post->saveMetaAt('author', 'Omar Rodriguez', '+1 year'); $post->meta->pluck('value', 'key'); /** * Illuminate\Support\Collection { * all: [ * "author" => "Anthony Kiedis", * "color" => "black", * ], * } */
有一个简写方法可以检索附加到模型上的所有当前元数据。这将包括所有明确定义的元键,默认为null
。
// Allow any meta key and explicitly allow `foo` and `bar`. $post->metaKeys(['*', 'foo', 'bar']); $post->saveMeta('foo', 'a value'); $post->saveMeta('another', true); $post->pluckMeta(); /** * Illuminate\Support\Collection { * all: [ * "foo" => "a value", * "bar" => null, * "another" => true, * ], * } */
如果您想检索已发布的所有元数据,请使用publishedMeta
关系。
// This array will also include `Jimi Hendrix´. $post->publishedMeta->toArray();
如果您想检查包括未发布记录在内的所有元数据,请使用allMeta
关系。
$post->allMeta->toArray();
您可以确定Meta
实例是否是相关模型的最新已发布记录,或者它尚未发布。
$meta = $post->allMeta->first(); $meta->is_current; // (bool) $meta->is_planned; // (bool)
查询Meta
模型
在Meta
模型本身上还有一些可能有助于的查询作用域。
Meta::published()->get(); // Only current and historic meta. Meta::planned()->get(); // Only meta not yet published. Meta::publishedBefore('+1 week')->get(); // Only meta published by next week. Meta::publishedAfter('+1 week')->get(); // Only meta still unpublished in a week. Meta::onlyCurrent()->get(); // Only current meta without planned or historic data. Meta::withoutHistory()->get(); // Query without stale records. Meta::withoutCurrent()->get(); // Query without current records.
默认情况下,这些函数将使用Carbon::now()
来确定哪些元数据被认为是最新的,但您也可以传递一个日期时间来查找。
// Get records that have been current a month ago. Meta::onlyCurrent('-1 month')->get(); // Get records that will not be history by tommorow. Meta::withoutHistory(Carbon::now()->addDay())->get();
通过元数据查询
查询元数据存在性
您可以查询具有给定键的元数据的记录。
// Find posts having at least one meta records for `color` key. Post::whereHasMeta('color')->get(); // Or pass an array to find records having meta for at least one of the given keys. Post::whereHasMeta(['color', 'background_color'])->get();
查询元数据不存在
您可以查询没有给定键的元数据的记录。
// Find posts not having any meta records for `color` key. Post::whereDoesntHaveMeta('color')->get(); // Or find records not having meta for any of the given keys. Post::whereDoesntHaveMeta(['color', 'background_color'])->get();
通过值查询元数据
您可以检索具有给定键和值的元数据的模型。
// Find posts where the current attached color is `black`. Post::whereMeta('color', 'black')->get(); // Find posts where the current attached color is not `black`. Post::whereMeta('color', '!=', 'black')->get(); // Find posts that are `visible`. Post::whereMeta('visible', true)->get(); // There are alternatives for building `or` clauses for all scopes. Post::whereMeta('visible', true)->orWhere('hidden', false)->get();
Multiplex将负责为传递的查询找到正确的数据类型。
// Matches only meta records with type `boolean`. Post::whereMeta('hidden', false)->get(); // Matches only meta records with type `datetime`. Post::whereMeta('release_at', '<=', Carbon::now())->get();
您也可以通过数组查询值。每个数组值将单独类型转换。
// Find posts where `color` is `black` (string) or `false` (boolean). Post::whereMetaIn('color', ['black', false])->get();
如果您想查询而不进行类型转换,请使用whereRawMeta()
。
Post::whereRawMeta('hidden', '')->get(); Post::whereRawMeta('likes', '>', '100')->get();
您还可以定义要使用的数据类型。
Post::whereMetaOfType('integer', 'count', '0')->get(); Post::whereMetaOfType('null', 'foo', '')->get();
查询空或非空元数据
您可以查询空或非空元数据,其中null
或空字符串被视为空。
Post::whereMetaEmpty('favorite_band')->get(); // Get all posts having meta names `likes` and `comments` where *both* of them are not empty. Post::whereMetaNotEmpty(['likes', 'comments'])->get();
事件
您可以监听Multiplex将引发的以下事件。
MetaHasBeenAdded
此事件将在将新版本的元保存到模型时触发一次。
use Kolossal\Multiplex\Events\MetaHasBeenAdded; class SomeListener { public function handle(MetaHasBeenAdded $event) { $event->meta; // The Meta model that was added. $event->model; // The parent model, same as $event->meta->metable. $event->type; // The class name of the parent model. } }
MetaHasBeenRemoved
此事件将在使用deleteMeta
删除元数据时触发一次。该事件将针对每个键只触发一次,事件上的$meta
属性将仅包含最新的元数据。
use Kolossal\Multiplex\Events\MetaHasBeenRemoved; class SomeListener { public function handle(MetaHasBeenRemoved $event) { $event->meta; // The Meta model that was removed. $event->model; // The parent model, same as $event->meta->metable. $event->type; // The class name of the parent model. } }
时间旅行
您可以获取模型在特定时间点的元数据。
$user = Auth::user()->withMetaAt('-1 week'); $user->favorite_band; // Tool $user->withMetaAt(Carbon::now())->favorite_band; // The Mars Volta
这样,您可以检查在特定时间有效的整个元数据集。
Post::first()->withMetaAt('2022-10-01 15:00:00')->meta->pluck('value', 'key');
您还可以按元数据查询特定时间点。
Post::travelTo(Carbon::now()->subWeeks(2))->whereMetaIn('foo', [false, 0])->get(); Post::travelTo(Carbon::now()->addYears(2))->where('category', 'tech')->get();
如果您想执行进一步的操作,请记住回溯。
Post::travelTo(Carbon::now()->subYear())->where('category', 'tech')->get(); Post::where('category', 'tech')->get(); // Will still look for meta published last year. Post::travelBack(); Post::where('category', 'tech')->get(); // Find current meta.
限制元数据键
您可以通过设置模型上的$metaKeys
来限制可用于元数据的键。
class Post extends Model { use HasMeta; protected array $metaKeys = [ 'color', 'hide', ]; }
默认情况下,所有键都被允许。
protected array $metaKeys = ['*'];
您还可以动态更改允许的元键。
$model->metaKeys(['color', 'hide']);
您还可以使用MetaAttribute
转换将属性转换为类型,这将自动允许该属性用作元键。
use Kolossal\Multiplex\MetaAttribute; class Post extends Model { use HasMeta; protected $metaKeys = []; protected $casts = [ 'body' => MetaAttribute::class, ]; }
尝试将值分配给不允许的元键将抛出Kolossal\Multiplex\Exceptions\MetaException
。
如果您已启用Eloquent严格性,则建议显式地将元属性转换为MetaAttribute
。
类型转换元键
有时您可能希望强制类型转换元属性。您可以通过定义特定元键应使用哪种类型来绕过猜测正确的类型。
protected array $metaKeys = [ 'foo', 'count' => 'integer', 'color' => 'string', 'hide' => 'boolean', ];
扩展数据库列
默认情况下,Multiplex 不会触摸你的模型列。但有时将元记录作为现有表列的扩展可能很有用。
考虑有一个仅包含 title
和 body
列的现有 Post
模型。通过显式将 body
添加到我们的元键数组 body
中,Multiplex 从现在起将处理它 - 不触摸 posts
表,而是使用数据库列作为后备。
class Post extends Model { use HasMeta; protected $metaKeys = [ '*', 'body', ]; }
\DB::table('posts')->create(['title' => 'A title', 'body' => 'A body.']); $post = Post::first(); $post->body; // A body. $post->body = 'This. Is. Meta.'; $post->save(); $post->body; // This. Is. Meta. $post->deleteMeta('body'); $post->body; // A body.
在将 Multiplex 用于扩展表列的情况下,Multiplex 在从数据库检索模型时会删除原始列,这样你就不需要过时的数据。
删除元数据
你可以从数据库中删除与模型关联的任何元数据。
// Delete all meta records for the `color` key. $post->deleteMeta('color'); // Or delete all meta records associated with the model. $post->purgeMeta();
性能
由于 Multiplex 将元数据存储在多态的 一对多 关系中,查询你的模型可能会很容易导致 N+1
查询问题。
根据你的用例,你应该考虑预加载 meta
关系,例如在你的模型上使用 $with
。如果你正在 扩展数据库列,这可能特别有用。
// Worst case: 26 queries if `color` is a meta value. $colors = Post::take(25)->get()->map( fn ($post) => $post->color; ); // Same result with only 2 queries. $colors = Post::with('meta')->take(25)->get()->map( fn ($post) => $post->color; );
配置
除了配置之外,无需进行任何设置,但如果你喜欢,你可以使用以下命令发布配置文件:
php artisan vendor:publish --tag="multiplex-config"
枚举支持
Multiplex 支持 PHP 8.1 中引入的 受支持枚举,而基本枚举将不起作用。
enum SampleEnum: string { case Hearts = 'hearts'; case Diamonds = 'diamonds'; } $model->saveMeta('some_key', SampleEnum::Diamonds); // true $model->some_key === SampleEnum::Diamonds;
UUID 和 ULID 支持
如果你的应用程序使用 UUID 或 ULID 作为模型(s)的元数据,你可以在运行迁移之前将 multiplex.morph_type
设置为 uuid
或 ulid
之前。如果你不想发布配置文件,你也可以设置 MULTIPLEX_MORPH_TYPE
环境变量。
这将确保 Meta
模型将使用 UUID/ULID,并且在运行迁移时使用适当的键和外键。
致谢
此包在很大程度上基于并受到 Laravel-Metable(由 Sean Fraser 创建)以及 laravel-meta(由 Kodeine 创建)的启发。使用 Package Skeleton(由伟大的 Spatie 创建)作为起点。