kolossal-io/laravel-multiplex

一个将版本化元数据附加到 Eloquent 模型的 Laravel 扩展包。

v1.5.0 2024-09-12 07:56 UTC

README


一个将时间切片元数据附加到 Eloquent 模型的 Laravel 扩展包。

Laravel Latest Version on Packagist GitHub Tests Action Status

查看目录

功能

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 不会触摸你的模型列。但有时将元记录作为现有表列的扩展可能很有用。

考虑有一个仅包含 titlebody 列的现有 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 设置为 uuidulid 之前。如果你不想发布配置文件,你也可以设置 MULTIPLEX_MORPH_TYPE 环境变量。

这将确保 Meta 模型将使用 UUID/ULID,并且在运行迁移时使用适当的键和外键。

致谢

此包在很大程度上基于并受到 Laravel-Metable(由 Sean Fraser 创建)以及 laravel-meta(由 Kodeine 创建)的启发。使用 Package Skeleton(由伟大的 Spatie 创建)作为起点。

许可




版权所有 © kolossal。在 MIT 许可证 下发布。