通过指令实现 IFTTT
Gato GraphQL 提供了通过指令实现 IFTTT(If This Then That)策略的能力。这些指令会在 query 中出现特定字段或指令时,被动态添加到 query 中。
一般而言,IFTTT 是一种在指定事件发生时触发操作的规则。在我们的场景中,事件与操作的对应关系如下:
- 「如果在 query 中发现字段 X」→「将指令 Y 附加到字段 X」
- 「如果在 query 中发现指令 Z」→「在指令 Z 前后执行指令 Y」
向模式动态添加 IFTTT 指令是一个递归过程:该指令本身也可以配置自己的一组 IFTTT 指令,这些指令同样会被添加到指令链中。
使用场景
在底层,Gato GraphQL 的客户端使用此机制来配置 GraphQL 模式。
例如,Access Control 允许我们选择要应用于操作、字段和指令的访问控制规则。正是通过 IFTTT,这些规则才得以应用于 GraphQL 模式中的相应元素。

以下是一些常见的使用场景:
按字段定义缓存控制 max-age
为所有字段附加 @CacheControl 指令,并自定义 maxAge 参数的值:Post 的 url 字段设置为 1 年,title 字段设置为 1 小时。
配置访问控制
为 User 类型的 email 字段附加 @validateDoesLoggedInUserHaveAnyRole 指令,使只有管理员才能查询用户的电子邮件地址。
同步访问控制与缓存控制
通过链式连接指令,可以确保在验证用户是否可以访问某个字段或指令时,响应不会被缓存。例如:
- 为
me字段附加@validateIsUserLoggedIn指令 - 为
@validateIsUserLoggedIn指令附加@CacheControl指令,并将maxAge参数值设为0
加强安全性
为 @translate 指令附加 @validateIsUserLoggedIn 指令,以防止恶意行为者向 GraphQL 服务执行 query,导致服务器宕机并产生高额费用(在此场景中,@translate 基于 Google Translate,使用该服务需要付费)。
工作原理
如何通过 IFTTT 向模式添加指令?举个例子,假设我们想创建一个自定义指令 @authorize(role: String!),用于验证执行字段 myPosts 的用户是否具有预期角色 author,否则显示错误。
如果使用 SDL 创建模式,它将如下所示:
directive @authorize(role: String!) on FIELD_DEFINITION
type User {
myPosts: [Post] @authorize(role: "author")
}IFTTT 规则定义了与上述 SDL 声明相同的意图:每当请求字段 myPosts 时,对其执行指令 @authorize(role: "author")。当在 query 中发现字段 myPosts 时,引擎会自动将 @authorize(role: 'author') 附加到可执行 query 中的该字段上。
IFTTT 规则也可以在遇到指令时触发,而不仅仅是字段。例如,可以设置规则「每当在 query 中发现指令 @translate 时,对该字段执行指令 @cache(time: 3600)」。
向 query 添加 IFTTT 指令是一个递归过程:它会触发新事件由 IFTTT 规则处理,可能向 query 附加其他指令,如此循环往复。
例如,规则「每当发现指令 @cache 时,执行指令 @log」会记录该字段执行的日志条目,并针对这个新添加的指令触发新事件。
通过 PHP 代码进行配置
User 类型有 roles 和 capabilities 字段,这些字段可能被视为敏感信息,因此不应允许普通用户访问。
我们可以为这两个字段附加 @validateDoesLoggedInUserHaveAnyRole 指令,配置为只有具有特定角色(通过环境变量配置)的用户才能访问它们。配置通过 CompilerPass 提供:
$accessControlManagerDefinition = $containerBuilderWrapper->getDefinition(AccessControlManagerInterface::class);
if ($roles = Environment::anyRoleLoggedInUserMustHaveToAccessRolesFields()) {
$accessControlManagerDefinition->addMethodCall(
'addEntriesForFields',
[
UserRolesAccessControlGroups::ROLES,
[
[RootObjectTypeResolver::class, 'roles', $roles],
[UserObjectTypeResolver::class, 'roles', $roles],
[RootObjectTypeResolver::class, 'capabilities', $roles],
[UserObjectTypeResolver::class, 'capabilities', $roles],
]
]
);
}
if ($capabilities = Environment::anyCapabilityLoggedInUserMustHaveToAccessRolesFields()) {
$accessControlManagerDefinition->addMethodCall(
'addEntriesForFields',
[
UserCapabilitiesAccessControlGroups::CAPABILITIES,
[
[RootObjectTypeResolver::class, 'roles', $capabilities],
[UserObjectTypeResolver::class, 'roles', $capabilities],
[RootObjectTypeResolver::class, 'capabilities', $capabilities],
[UserObjectTypeResolver::class, 'capabilities', $capabilities],
]
]
);
}执行 query 时,未登录的用户以及不具备所需角色的用户将不被允许访问这些字段。