tonysm/globalid-laravel

使用URI标识应用模型。受globalid gem启发。

1.2.0 2024-03-08 01:14 UTC

This package is auto-updated.

Last update: 2024-09-08 03:40:49 UTC


README

GlobalIds Laravel

Total Downloads License

简介

使用URI标识应用模型。

全局ID是应用范围内唯一标识模型实例的URI。

gid://YourApp/Some\\Model/id

当需要单一标识符来引用不同类别的对象时,这非常有用。

一个例子是将模型引用存储在无法强制约束或无法使用方便的Eloquent关系的文本内容字段中。我们需要引用模型对象而不是序列化对象本身。我们可以在渲染富文本内容时使用全局ID来定位模型。渲染不需要知道模型命名和ID的详细信息,只需要知道它有一个引用模型的全球标识符。

另一个例子是一个包含用户和组选项的下拉列表。通常我们需要想出一个临时的方案来引用它们。使用全局ID,我们有一个适用于这两个类对象的通用标识符。

灵感来源

主要受到globalid gem的启发。

安装

通过Composer

composer require tonysm/globalid-laravel

用法

HasGlobalIdentification特性添加到任何Eloquent模型(或任何具有find($id)findMany($ids): Collection静态方法和getKey()实例方法的类)

use Tonysm\GlobalId\Models\HasGlobalIdentification;

class Person extends Model
{
    use HasGlobalIdentification;
}

然后您可以这样创建GlobalIds和SignedGlobalIds

$personGid = Person::find(1)->toGlobalId();
# => Tonysm\GlobalId\GlobalId {#5010}

$personGid->toString();
# => "gid://laravel/App%5CModels%5CPerson/1"

# Returns a URL-safe base64 encoded version of the SGID...
$personGid->toParam();
# => "Z2lkOi8vbGFyYXZlbC9BcHAlNUNNb2RlbHMlNUNQZXJzb24vMQ"

Tonysm\GlobalId\Facades\Locator::locate('gid://laravel/App%5CModels%5CPerson/1');
# => App\Models\Person {#5022 id:1...

# You can also pass the base64 encoded to it and it will just work...
Tonysm\GlobalId\Facades\Locator::locate('Z2lkOi8vbGFyYXZlbC9BcHAlNUNNb2RlbHMlNUNQZXJzb24vMQ');
# => App\Models\Person {#5022 id:1...

# You can also call the locate method on the GlobalId object...
$personGid->locate();
# => App\Models\Person {#5022 id:1...

您可以使用自定义定位器来自定义位置逻辑。

签名全局ID

为了增加安全性,全局ID也可以进行签名以确保数据未被篡改。

$personSgid = Person::find(1)->toSignedGlobalId();
# => Tonysm\GlobalId\SignedGlobalId {#5005}

$personSgid = Person::find(1)->toSgid();
# => Tonysm\GlobalId\SignedGlobalId {#5026}

$personSgid->toString();
# => "BAhJIh5naWQ6Ly9pZGluYWlkaS9Vc2VyLzM5NTk5BjoGRVQ=--81d7358dd5ee2ca33189bb404592df5e8d11420e"

Tonysm\GlobalId\Facades\Locator::locateSigned($personSgid);
# => App\Models\Person {#5009 id: 1, ...

# You can also call the locate method on the SignedGlobalId object...
$personSgid->locate();
# => App\Models\Person {#5022 id:1...

过期时间

签名全局ID可以在未来某个时间点过期。如果人们不应该无限期地访问某些资源,如共享链接,这非常有用。

$expiringSgid = Document::find(5)->toSgid([
    'expires_at' => now()->addHours(2),
    'for' => 'sharing',
]);
# => Tonysm\GlobalId\SignedGlobalId {#5026}

# Within 2 hours...
Tonysm\GlobalId\Facades\Locator::locateSigned($expiringSgid->toString(), [
    'for' => 'sharing',
]);
# => App\Models\Document {#5009 id: 5, ...

# More than 2 hours later...
Tonysm\GlobalId\Facades\Locator::locateSigned($expiringSgid->toString(), [
    'for' => 'sharing',
]);
# => null

默认情况下,自动过期时间为1个月。您可以通过从任何服务提供者的启动方法中传递一个过期解析器Closure来覆盖此默认值。此解析器将在每次创建SGID时被调用

SignedGlobalId::useExpirationResolver(() => now()->addMonths(3));

这样,任何生成的SGID都将使用该相对过期时间。

需要注意的是,过期的SGID不是幂等的,因为它们编码了当前的时间戳;对to_sgid的重复调用将产生不同的结果。例如

Document::find(5)->toSgid()->toString() == Document::find(5)->toSgid()->toString()
# => false

您需要显式传递['expires_at' => null]来生成一个永久SGID,该SGID不会过期

# Passing a false value to either expiry option turns off expiration entirely.
$neverExpiringSgid = Document::find(5)->toSgid(['expires_at' => null]);
# => Tonysm\GlobalId\SignedGlobalId {#5026}

# Any time later...
Tonysm\GlobalId\Facades\Locator::locateSigned($neverExpiringSgid);
# => App\Models\Document {#5009 id: 5, ...

目的

您甚至可以通过说明签名全局ID的用途来进一步提高安全性。这样,坏人就不能在登录页面重复使用注册表单的SGID。例如

$signupPersonSgid = Person::find(1)->toSgid(['for' => 'signup_form']);
# => Tonysm\GlobalId\SignedGlobalId {#5026}

Tonysm\GlobalId\Facades\Locator::locateSigned($signupPersonSgid, ['for' => 'signup_form']);
# => App\Models\Person {#5009 id: 1, ...

Tonysm\GlobalId\Facades\Locator::locateSigned($signupPersonSgid, ['for' => 'login']);
# => null

定位多个全局ID

当需要定位多个全局ID时,请使用Tonysm\GlobalId\Facades\Locator->locateManyTonysm\GlobalId\Facades\Locator::locateManySigned()(对于签名全局ID)以允许更有效地加载全局ID。

例如,默认定位器为每个$modelName传递每个$modelId,因此使用$modelName::findMany($ids),而不是Tonysm\GlobalId\Facades\Locator->locate()$modelName::find($id)

在从数据库中查找全局ID的情况下,只需按如下所示查询每个$modelName一次即可

$gids = $users->merge($students)->sortBy('id')->map(fn ($model) => $model->toGlobalId());
# => [#<Tonysm\GlobalId\GlobalId {#5026} @gid=#GID<gid://app/User/1>>,
#<Tonysm\GlobalId\GlobalId {#5027} @gid=#GID<gid://app/Student/1>>,
#<Tonysm\GlobalId\GlobalId {#5028} @gid=#<GID gid://app/User/2>>,
#<Tonysm\GlobalId\GlobalId {#5029} @gid=#<GID gid://app/Student/2>>,
#<Tonysm\GlobalId\GlobalId {#5030} @gid=#<GID gid://app/User/3>>,
#<Tonysm\GlobalId\GlobalId {#5031} @gid=#<GID gid://app/Student/3>>]

Tonysm\GlobalId\Facades\Locator::locateMany($gids);
# SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]
# SELECT "students".* FROM "students" WHERE "students"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]
# => [#<User id: 1>, #<Student id: 1>, #<User id: 2>, #<Student id: 2>, #<User id: 3>, #<Student id: 3>]

注意返回结果的顺序保持不变。

自定义应用定位器

可以通过调用 Tonysm\GlobalId\Locator::use() 并提供一个用于该应用的定位器来为应用设置自定义定位器。当不同应用协作并引用彼此的全局ID时,自定义应用定位器非常有用。在查找全局ID的模型时,所使用的定位器基于全局ID URL中提供的应用名称。

使用自定义定位器

use Tonysm\GlobalId\GlobalId;
use Tonysm\GlobalId\Facades\Locator;
use Tonysm\GlobalId\Locators\LocatorContract;
use Illuminate\Support\Collection;

Locator::use('foo', new class implements LocatorContract {
    public function locate(GlobalId $globalId)
    {
        // ...
    }

    public function locateMany(Collection $globalIds, array $options = []): Collection
    {
        // ...
    }
});

定义好定位器后,像 gid://foo/Person/1 这样的URI现在将使用该定位器。其他应用仍然会继续使用默认定位器。

自定义多态类型

当使用Eloquent的 自定义多态类型 功能时,GID URI内部的模型名称将使用您的别名而不是模型的完全限定类名。

use App\Models\Person;

Relation::enforceMorphMap([
    'person' => Person::class,
]);

$gid = GlogalId::create(Person::create(['name' => 'a person']), [
    'app' => 'laravel',
]);

$gid->toString();
# => "gid://laravel/person/1"

测试包

composer test

变更日志

有关最近更改的更多信息,请参阅变更日志

贡献

鼓励您提交拉取请求,提出功能建议和讨论问题。

安全漏洞

如果您想报告安全漏洞,请给我发送电子邮件至 tonysm@hey.com

许可

MIT许可证(MIT)。有关更多信息,请参阅许可文件

鸣谢