tonysm / globalid-laravel
使用URI标识应用模型。受globalid gem启发。
Requires
- php: ^8.0
- illuminate/contracts: ^8.47|^9.0|^10.0|^11.0
- spatie/laravel-package-tools: ^1.9.2
Requires (Dev)
- nunomaduro/collision: ^7.0|^8.1
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.5
- vimeo/psalm: ^5.2
README
简介
使用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->locateMany
或Tonysm\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)。有关更多信息,请参阅许可文件。