WordPress 是一款用 PHP 编写的 CMS。但是,即使 PHP 是基础,WordPress 也秉持着将用户需求优先于开发者便利性的理念。这种理念在构建 WordPress 主题和插件的开发者与管理 WordPress 网站的用户之间建立了一个隐含的契约。
GraphQL 是一种从服务器检索数据并可以向服务器提交数据的接口。GraphQL 服务器可以对其如何实现 GraphQL 规范有自己的主见,以便将某些特定行为优先于其他行为。
依赖于服务器端架构的 WordPress 理念能否与通过 API 传输数据的基于 JavaScript 的查询语言共存?
让我们来分解这个问题,并解释一下我编写的 GraphQL API WordPress 插件 如何在这两种架构之间建立桥梁。
您可能知道 WPGraphQL。插件 **WordPress 的 GraphQL API**(或简称“GraphQL API”)是 WordPress 的一个不同的 GraphQL 服务器,具有 不同的功能。
在 GraphQL 服务中协调 WordPress 理念
此表包含 WordPress 应用程序或插件的预期行为,以及如何在 WordPress 上运行的 GraphQL 服务中解释它
类别 | WordPress 应用程序预期行为 | 在 WordPress 上运行的 GraphQL 服务的解释 |
---|---|---|
访问数据 | 民主化发布:任何用户(无论是否具备技术技能)都必须能够使用该软件 | 民主化数据访问和发布:任何用户(无论是否具备技术技能)都必须能够可视化和修改 GraphQL 模式,并执行 GraphQL 查询 |
可扩展性 | 应用程序必须可以通过插件扩展 | GraphQL 模式必须可以通过插件扩展 |
动态行为 | 应用程序的行为可以通过钩子修改 | 解析查询的结果可以通过指令修改 |
本地化 | 应用程序必须本地化,以便任何地区、任何语言的人使用 | GraphQL 模式必须本地化,以便任何地区、任何语言的人使用 |
用户界面 | 安装和操作功能必须通过用户界面完成,尽可能少地使用代码 | 向 GraphQL 模式添加新实体(类型、字段、指令)、配置它们、执行查询以及定义访问服务的权限必须通过用户界面完成,尽可能少地使用代码 |
访问控制 | 可以通过用户角色和权限授予对功能的访问权限 | 可以通过用户角色和权限授予对 GraphQL 模式的访问权限 |
防止冲突 | 开发者事先不知道谁会使用他们的插件,或者这些站点将在什么配置/环境中运行,这意味着插件必须为冲突做好准备(例如,有两个插件定义了 SMTP 服务),并尽可能地尝试防止它们 | 开发者事先不知道谁会访问和修改 GraphQL 模式,或者这些站点将在什么配置/环境中运行,这意味着插件必须为冲突做好准备(例如,有两个插件在 GraphQL 模式中具有相同的类型名称),并尽可能地尝试防止它们 |
让我们看看 GraphQL API 如何实现这些想法。
访问数据
与 REST 类似,GraphQL 服务必须通过 PHP 函数进行编码。谁来做这个,怎么做?
通过代码更改 GraphQL 模式
GraphQL 模式包括 类型、字段 和 指令。这些通过解析器处理,解析器是 PHP 代码片段。谁应该创建这些解析器?
最佳策略是让 GraphQL API 已经满足包含 WordPress 中所有已知实体(包括帖子、用户、评论、类别和标签)的基本 GraphQL 模式,并简化引入新解析器的方法,例如自定义帖子类型 (CPT)。
这就是插件如何提供用户实体。User
类型通过 此代码提供
class UserTypeResolver extends AbstractTypeResolver
{
public function getTypeName(): string
{
return 'User';
}
public function getSchemaTypeDescription(): ?string
{
return __('Representation of a user', 'users');
}
public function getID(object $user)
{
return $user->ID;
}
public function getTypeDataLoaderClass(): string
{
return UserTypeDataLoader::class;
}
}
类型解析器不会直接从数据库加载对象,而是将此任务委托给 TypeDataLoader
对象(在上面的示例中,来自 UserTypeDataLoader
)。这种解耦是为了遵循 SOLID 原则,提供不同的实体来处理不同的责任,以便使代码易于维护、可扩展和易于理解。
向 User
类型添加 username
、email
和 url
字段是通过 FieldResolver
对象完成的
class UserFieldResolver extends AbstractDBDataFieldResolver
{
public static function getClassesToAttachTo(): array
{
return [
UserTypeResolver::class,
];
}
public static function getFieldNamesToResolve(): array
{
return [
'username',
'email',
'url',
];
}
public function getSchemaFieldDescription(
TypeResolverInterface $typeResolver,
string $fieldName
): ?string {
$descriptions = [
'username' => __("User's username handle", "graphql-api"),
'email' => __("User's email", "graphql-api"),
'url' => __("URL of the user's profile in the website", "graphql-api"),
];
return $descriptions[$fieldName];
}
public function getSchemaFieldType(
TypeResolverInterface $typeResolver,
string $fieldName
): ?string {
$types = [
'username' => SchemaDefinition::TYPE_STRING,
'email' => SchemaDefinition::TYPE_EMAIL,
'url' => SchemaDefinition::TYPE_URL,
];
return $types[$fieldName];
}
public function resolveValue(
TypeResolverInterface $typeResolver,
object $user,
string $fieldName,
array $fieldArgs = []
) {
switch ($fieldName) {
case 'username':
return $user->user_login;
case 'email':
return $user->user_email;
case 'url':
return get_author_posts_url($user->ID);
}
return null;
}
}
可以观察到,GraphQL 模式的字段定义及其解析已拆分为多个函数
getSchemaFieldDescription
getSchemaFieldType
resolveValue
其他函数包括
getSchemaFieldArgs
:声明字段参数(包括其名称、描述、类型以及它们是否为必填)isSchemaFieldResponseNonNullable
:指示字段是否为非空getImplementedInterfaceClasses
:定义字段实现的 接口 的解析器resolveFieldTypeResolverClass
:当字段是连接时定义类型解析器resolveFieldMutationResolverClass
:当字段执行 突变时定义解析器
如果所有功能都通过单个函数或配置数组来满足,那么这段代码的可读性会更高,从而使解析器的实现和维护更容易。
检索插件或自定义 CPT 数据
当插件没有通过创建新的类型和字段解析器将其数据集成到 GraphQL 模式时会发生什么?用户是否可以通过 GraphQL 查询来自此插件的数据?
例如,假设 WooCommerce 具有用于产品的 CPT,但它没有将相应的 Product
类型引入 GraphQL 模式。是否可以检索产品数据?
关于 CPT 实体,它们的数据可以通过类型 GenericCustomPost
提取,它充当一种通配符,以包含站点中安装的任何自定义帖子类型。通过查询 Root.genericCustomPosts(customPostTypes: [cpt1, cpt2, ...])
来检索记录(在此字段表示法中,Root
是类型,genericCustomPosts
是字段)。
然后,要提取名称为 "wc_product"
的 CPT 对应的产品数据,我们执行以下查询
{
genericCustomPosts(customPostTypes: "[wc_product]") {
id
title
url
date
}
}
但是,所有可用字段仅是每个 CPT 实体中存在的那些字段:title
、url
、date
等。如果产品的 CPT 具有价格数据,则相应的字段 price
不可用。wc_product
指的是 WooCommerce 插件创建的 CPT,因此,WooCommerce 或网站的开发者必须实现 Product
类型,并定义其自己的自定义字段。
CPT 通常用于管理私有数据,这些数据不得通过 API 公开。出于这个原因,GraphQL API 最初仅公开 Page
类型,并要求定义哪些其他 CPT 的数据可以公开查询

从 REST 迁移到 GraphQL 通过持久化查询
虽然 GraphQL 作为插件提供,但 WordPress 通过 WP REST API 内置支持 REST。在某些情况下,使用 WP REST API 的开发人员可能会发现迁移到 GraphQL 存在问题。
例如,考虑以下差异
- REST 端点有自己的 URL,可以通过
GET
进行查询,而 GraphQL 通常通过单个端点运行,仅通过POST
进行查询 - REST 端点可以在服务器端缓存(通过
GET
查询时),而 GraphQL 端点通常无法缓存
因此,REST 提供了更好的开箱即用的缓存支持,使应用程序性能更佳并减少服务器负载。相反,GraphQL 更侧重于客户端的缓存,如 Apollo 客户端 所支持的那样。
切换到 GraphQL 后,开发人员是否需要重新构建客户端的应用程序,引入 Apollo 客户端仅仅是为了引入一层缓存?这将令人遗憾。
“持久化查询”功能 为这种情况提供了解决方案。持久化查询将 REST 和 GraphQL 结合在一起,使我们能够
- 使用 GraphQL 创建查询,以及
- 将查询发布到自己的 URL 上,类似于 REST 端点。
持久化查询端点与 REST 端点具有相同的行为:可以通过 GET
访问,并且可以在服务器端缓存。但它是使用 GraphQL 语法创建的,公开的数据没有欠获取或过获取。
可扩展性
GraphQL API 的架构将决定添加我们自己的扩展的难易程度。
解耦类型和字段解析器
GraphQL API 使用 发布-订阅模式 使字段“订阅”类型。
重新评估之前的字段解析器
class UserFieldResolver extends AbstractDBDataFieldResolver
{
public static function getClassesToAttachTo(): array
{
return [UserTypeResolver::class];
}
public static function getFieldNamesToResolve(): array
{
return [
'username',
'email',
'url',
];
}
}
User
类型事先不知道它将满足哪些字段,但这些(username
、email
和 url
)是由字段解析器注入到类型的。
这样,GraphQL 模式就变得易于扩展。通过简单地添加一个字段解析器,任何插件都可以向现有类型添加新字段(例如 WooCommerce 为 User.shippingAddress
添加一个字段),或覆盖字段的解析方式(例如重新定义 User.url
以返回用户的网站)。
代码优先方法
插件必须能够扩展 GraphQL 模式。例如,它们可以提供一个新的 Product
类型,在 Post
类型上添加一个额外的 coauthors
字段,提供一个 @sendEmail
指令,或任何其他内容。
为了实现这一点,GraphQL API 遵循 **代码优先方法**,其中模式是在运行时从 PHP 代码生成的。
另一种方法称为 **SDL 优先**(模式定义语言),它要求模式提前提供,例如,通过一些 .gql
文件。
这两种方法之间的 主要区别在于,在代码优先方法中,GraphQL 模式是动态的,可以适应不同的用户或应用程序。这适合 WordPress,因为单个站点可以为多个应用程序(例如网站和移动应用程序)提供支持,并可以针对不同的客户进行自定义。GraphQL API 通过 “自定义端点”功能 明确了此行为,该功能能够为不同的用户或应用程序创建具有访问不同 GraphQL 模式的不同端点。
为了避免性能下降,模式通过将其缓存到磁盘或内存中使其静态,并在安装扩展模式的新插件或管理员更新设置时重新生成。
支持新功能
使用代码优先方法的另一个好处是,它使我们能够提供全新的功能,可以在这些功能获得 GraphQL 规范 支持之前选择使用。
例如,嵌套突变 已在规范中提出请求,但尚未批准。GraphQL API 符合规范,使用类型 QueryRoot
和 MutationRoot
分别处理查询和突变,如 标准模式中所示。但是,通过启用可选的 “嵌套突变”功能,模式将发生转换,查询和突变将改为由单个 Root
类型 处理,从而支持嵌套突变。
让我们看看此新功能的实际应用。在 此查询中,我们首先通过 Root.post
查询帖子,然后在其上执行突变 Post.addComment
并获取创建的评论对象,最后在其上执行突变 Comment.reply
并查询其一些数据(取消第一个突变的注释以登录用户,以便允许添加评论)。
# mutation {
# loginUser(
# usernameOrEmail:"test",
# password:"pass"
# ) {
# id
# name
# }
# }
mutation {
post(id:1459) {
id
title
addComment(comment:"That's really beautiful!") {
id
date
content
author {
id
name
}
reply(comment:"Yes, it is!") {
id
date
content
}
}
}
}
动态行为
WordPress 使用 钩子(过滤器和操作)来修改行为。钩子是简单的代码片段,可以在触发时覆盖值或启用执行自定义操作。
GraphQL 中是否存在等效项?
指令以覆盖功能
在寻找 GraphQL 的类似机制时,我得出结论,指令在某种程度上可以被认为等同于 WordPress 钩子:就像过滤器钩子一样,指令是一个修改字段值的函数,从而增强其他一些功能。
例如,假设我们使用 此查询 检索帖子标题列表
query {
posts {
title
}
}
…这将产生以下响应
{
"data": {
"posts": [
{
"title": "Scheduled by Leo"
},
{
"title": "COPE with WordPress: Post demo containing plenty of blocks"
},
{
"title": "A lovely tango, not with leo"
},
{
"title": "Hello world!"
},
]
}
}
这些结果为英文。如何将其翻译成西班牙语?在 title
字段上应用指令 @translate
(通过 此指令解析器 实现),它将字段的值作为输入,调用 Google Translate API 进行翻译,并使其结果覆盖原始输入,如 此查询 中所示。
query {
posts {
title @translate(from:"en", to"es")
}
}
…这将产生以下响应
{
"data": {
"posts": [
{
"title": "Programado por Leo"
},
{
"title": "COPE con WordPress: publica una demostración que contiene muchos bloques"
},
{
"title": "Un tango lindo, no con leo"
},
{
"title": "¡Hola Mundo!"
}
]
}
}
请注意,指令与输入来自哪里无关。在本例中,它是 Post.title
字段,但它也可能是 Post.excerpt
、Comment.content
或任何其他类型为 String
的字段。然后,解析字段并覆盖其值被干净地解耦,并且指令始终可重用。
指令连接到第三方
随着 WordPress 稳步成为网络的操作系统(目前 为 39% 的所有网站提供支持,超过任何其他软件),它也逐渐增加了与外部服务的交互(例如 Stripe 用于支付,Slack 用于通知,AWS S3 用于托管资产,等等)。
如上所述,指令可用于覆盖字段的响应。但新值从哪里来?它可以来自某些本地函数,但它也可以完全来自某些外部服务(例如我们之前看到的指令 @translate
,它从 Google Translate API 检索新值)。
因此,GraphQL API 决定简化指令与外部 API 通信的方式,使这些服务能够在执行查询时转换来自 WordPress 站点的数据,例如用于
- 翻译,
- 图像压缩,
- 通过 CDN 获取资源,以及
- 发送电子邮件、短信和 Slack 通知。
事实上,GraphQL API 决定尽可能地增强指令的功能,使其成为服务器架构中的低级组件,甚至使查询解析本身也基于指令管道。这赋予指令强大的能力,例如执行授权、验证和修改响应等。
本地化
使用 SDL-first 方法的 GraphQL 服务器难以本地化模式中的信息(规范的相关问题 四年前就被提出,至今仍未解决)。
然而,使用 code-first 方法,GraphQL API 可以通过 __('some text', 'domain')
PHP 函数以简单的方式本地化描述,并且本地化字符串将从 WordPress 后台选择的区域和语言对应的 POT 文件中检索。
例如,正如我们之前看到的,这段代码本地化了字段描述
class UserFieldResolver extends AbstractDBDataFieldResolver
{
public function getSchemaFieldDescription(
TypeResolverInterface $typeResolver,
string $fieldName
): ?string {
$descriptions = [
'username' => __("User's username handle", "graphql-api"),
'email' => __("User's email", "graphql-api"),
'url' => __("URL of the user's profile in the website", "graphql-api"),
];
return $descriptions[$fieldName];
}
}
用户界面
GraphQL 生态系统充满了用于与服务交互的开源工具,其中许多工具提供了 WordPress 中预期的相同用户友好体验。
使用GraphQL Voyager可以可视化 GraphQL 模式。

在创建自己的 CPT 和查看它们如何以及从何处访问以及为它们公开了哪些数据时,这将特别有用。

使用GraphiQL可以对 GraphQL 端点执行查询。

但是,此工具对所有人来说都不够简单,因为用户必须了解 GraphQL 查询语法。因此,此外,还安装了GraphiQL Explorer,以便通过点击字段来组合 GraphQL 查询。

访问控制
WordPress 提供不同的用户角色(管理员、编辑、作者、投稿者和订阅者)来管理用户权限,用户可以登录到 wp-admin
(例如:工作人员),登录到面向公众的网站(例如:客户),或者未登录或拥有帐户(任何访客)。GraphQL API 必须考虑这些情况,允许授予不同用户细粒度的访问权限。
授予工具的访问权限
GraphQL API 允许配置谁可以访问 GraphiQL 和 Voyager 客户端以可视化模式并对其执行查询。
- 只有管理员吗?
- 工作人员?
- 客户?
- 对所有人开放访问?
出于安全原因,插件默认情况下仅提供对管理员的访问权限,并且不会在互联网上公开暴露服务。
在上一节的图片中,GraphiQL 和 Voyager 客户端可在 wp-admin
中使用,仅限管理员用户访问。管理员用户可以通过设置授予其他角色(编辑、作者、投稿者)的用户访问权限。

为了授予我们的客户或互联网上的任何人访问权限,我们不想让他们访问 WordPress 后台。然后,设置启用在新的面向公众的 URL 下公开工具(例如 mywebsite.com/graphiql
和 mywebsite.com/graphql-interactive
)。公开这些公共 URL 是管理员明确设置的选择。

授予 GraphQL 模式的访问权限
WP REST API 并不容易自定义谁可以访问某个端点或端点内的某个字段,因为它没有提供用户界面,必须通过代码实现。
相反,GraphQL API 利用 GraphQL 模式中已有的元数据,通过用户界面(由 WordPress 编辑器提供支持)启用服务的配置。因此,非技术用户也可以管理他们的 API,而无需编写一行代码。
管理对模式中不同字段(和指令)的访问控制是通过单击它们并在下拉菜单中选择哪些用户(例如已登录的用户或具有特定功能的用户)可以访问它们来完成的。

防止冲突
当两个插件对它们的类型使用相同的名称时,命名空间有助于避免冲突。例如,如果 WooCommerce 和 Easy Digital Downloads 都实现了一个名为 Product
的类型,那么执行查询以获取产品将变得模棱两可。然后,命名空间将类型名称转换为 WooCommerceProduct
和 EDDProduct
,从而解决冲突。
不过,发生此类冲突的可能性并不高。因此,最佳策略是默认情况下将其禁用(以使模式尽可能简单),仅在需要时启用。
如果启用,GraphQL 服务器将使用相应的 PHP 包名称自动为类型命名空间(所有包都遵循 PHP 标准建议 PSR-4
)。例如,对于此常规 GraphQL 模式

…如果启用了命名空间,则 Post
将变为 PoPSchema_Posts_Post
,Comment
将变为 PoPSchema_Comments_Comment
,依此类推。

就是这样
WordPress 和 GraphQL 本身都是引人入胜的话题,因此我发现 WordPress 和 GraphQL 的集成非常令人喜爱。在从事这项工作几年后,我可以说,设计一种最佳方式让旧的 CMS 管理内容,并让新的界面访问它,是一项值得追求的挑战。
我可以继续描述 WordPress 理念如何影响在 WordPress 上运行的 GraphQL 服务的实现,甚至可以谈论几个小时,使用大量我没有包含在此文章中的材料。但我需要停下来……所以我现在就停下来。
我希望本文能够很好地概述了在 GraphQL 中满足 WordPress 理念的原因和方法,正如插件WordPress 的 GraphQL API 所做的那样。
这看起来很有前景,尽管以这种方式创建 WordPress 网站可能需要更多工作。但是,在速度方面,我觉得这是未来不久的发展方向。所以我会继续尝试这个。
总是很有趣地看到你在做什么,Leo。期待尝试一下。