为 WordPress 站点、主题或插件映射 GraphQL Schema
您已决定开始在现有的 WordPress 站点中使用 GraphQL。太棒了!无论用于新功能还是现有功能,GraphQL 都需要与底层数据层进行交互,因此您需要将应用程序的数据模型(WordPress 站点中的自定义 PHP 代码、主题或插件)映射到 GraphQL schema 中。
应该如何进行映射?必须一次性完成吗?应该是现有数据模型的完整副本吗?在此过程中调整一些不合适的名称又该怎么办?关于技术债务,应该保留还是处理?
让我们探索一些将现有 WordPress 应用程序的数据模型映射到 GraphQL schema 的策略。
按照自己的节奏映射 schema
向应用程序添加 GraphQL 并非全有或全无的选择。同一个应用程序可以同时由多个 API 提供支持,在这种情况下,GraphQL 将与其他 API 共存,持续所需的时间。例如,我们可以让现有功能继续由 REST 驱动,而仅将 GraphQL 用于所有新功能。
如果您想完全迁移到 GraphQL,也不必一次性完成。现有功能可以慢慢但稳步地迁移到 GraphQL,直到有一天 GraphQL 成为应用程序中唯一的 API。
因此,尽管您可以在第一天就创建完整的 GraphQL schema,但并非必须如此:在任何给定时间,只有功能所需的实体必须出现在 schema 上(通过其类型、字段和接口)。您可以随着时间推移,以渐进的方式逐步映射它们。
不要让接口承担实现的负担
GraphQL 服务器将实现访问应用程序数据的逻辑。它通过调用 WordPress 的功能来实现,例如调用 get_posts 来检索文章数据。在这一层,存在满足解析器的 PHP 代码。
然而,GraphQL schema 是一个接口:它声明了在 API 中访问数据的契约。它不关心实现细节:它对 WordPress、函数 get_posts、数据库表 wp_posts 或 SQL 查询一无所知。
因此,我们应尽量避免层与层之间的信息泄漏。
这一点很重要,因为数据模型往往会因其实现而受到污染。WordPress 通过 "attachment" CPT 提供了一个清晰的例子,用于表示图像等媒体文件。
由于它是一个自定义文章类型,图像被视为文章。因此,我们可能会倾向于使用 Post 类型来表示媒体文件,该类型包含以下字段:
type Post {
id: ID!
title: String
content: String
excerpt: String
}但这对于应用程序来说可能并不合适。"content" 字段的含义对于文章来说是清晰的,但对于图像来说则不然。它很可能不应该属于那里。
图像在 WordPress 中被建模为 CPT,是因为这样做很方便,可以复用现有逻辑,并存储在现有的 wp_posts 表中。
然而,方便并不意味着合适,最终可能导致技术债务(即存在缺陷的代码,如果不产生破坏性变更就无法修复,因此在应用程序中保留的时间比应有的更长)。
我们尽量不希望在应用程序中积累技术债务。只要有机会,就应该修复它。将数据模型映射到 GraphQL schema 提供了这样的机会,使我们能够在数据接口层纠正问题。
(不过,技术债务在应用程序层面仍然存在,所以我们并没有完全解决问题,只是在力所能及的范围内减轻了它。)
让我们将这个想法付诸实践。与其使用 Post 类型来表示媒体文件,不如使用 Media 类型,其中只包含对图像实体有意义的属性:
type Media {
id: ID!
src: String!
width: Int
height: Int
}在底层实现层面,字段解析器仍然会执行 get_posts 函数来解析 Media 类型的条目,但这与 GraphQL schema 无关。
将 GraphQL schema 与数据库图解耦
WordPress 基于这个数据库实体关系图实现:

我们必须让 GraphQL schema 基于数据库图,但不应尝试创建 1 对 1 的副本。这是因为 GraphQL schema 和数据库图都是在某些先决条件或限制下构建的,这些条件或限制不适用于另一方。
上一节演示了一个例子,其中表 wp_posts 存储图像 CPT 的数据,但在 GraphQL 中会有两种不同的类型:Post 和 Media。
再考虑另一个例子:分类目录。在 WordPress 中,文章可以有一个(或多个)分类目录,任何 CPT 也可以创建自己的分类目录。例如,名为 "event" 的 CPT 将有一个 "event_category"。
文章分类目录和事件分类目录都存储在 wp_terms 表中。这使得 WordPress 在执行 SQL 查询时,可以轻松地从任一分类目录类型中获取行。
因此,我们可能会倾向于通过 Category 类型来映射分类目录,由文章和事件共同引用:
type Category {
id: ID!
name: String!
}
type Post {
categories: [Category]!
}
type Event {
categories: [Category]!
}然而,文章将始终包含文章分类目录,而事件将始终包含事件分类目录。这两种分类目录类型的数据可能存储在同一个数据库表中,但在应用程序层面不会混合。文章分类目录和事件分类目录是两个不同的实体。
GraphQL 有一个静态类型系统。为了充分利用 GraphQL,应用程序层面的不同实体必须在 GraphQL schema 中使用不同的类型建模。
在这种情况下,将分类目录映射到 GraphQL schema 时,我们应该为每个分类目录创建不同的类型:PostCategory 和 EventCategory。然后,Post 类型只引用 PostCategory,Event 类型只引用 EventCategory:
type PostCategory {
id: ID!
name: String!
}
type Post {
categories: [PostCategory]!
}
type EventCategory {
id: ID!
name: String!
}
type Event {
categories: [EventCategory]!
}如果我们仍然希望 schema 中有一个包含所有分类目录的实体,可以通过接口 Category 来实现:
interface Category {
name: String!
}
type PostCategory implements Category {
id: ID!
name: String!
}
type EventCategory implements Category {
id: ID!
name: String!
}这样,访问 API 的用户将清楚地了解将检索哪些数据,而不必关心它在数据库图中如何映射,以及如何存储在数据库中。
一旦我们完成最终的 GraphQL schema,就会发现其形状在某种程度上类似于 WordPress 数据库图,但又明显不同:

遵循静态类型,调整字段命名
字段应尽可能地遵守其在应用程序中的命名。
例如,我们可以使用函数 wp_insert_post 创建文章,文章具有 "title" 和 "content" 属性。这些名称对于 GraphQL schema 也很合适(尽管可能需要稍作修改),因此我们应该保留它们:
type MutationRoot {
insertPost(title: String, content: String): Post
}
type Post {
id: ID!
title: String
content: String
}但情况并非总是如此。正如我们之前看到的,自定义文章必须解耦到各自的实体中。因此,虽然函数 get_posts 检索任何 CPT 的列表,但 schema 根类型中的等效字段 posts 只会检索 Post 类型的实体,而不包括 Page(它也是一种 CPT):
type QueryRoot {
posts: [Post]!
}那么,我们如何获取所有文章和页面的列表呢?通过另一个字段 customPosts,它检索在联合类型 CustomPostUnion 下映射的任何 CPT 的实体:
union CustomPostUnion = Post | Page
type QueryRoot {
customPosts: [CustomPostUnion]!
}重要的教训是:我们为 GraphQL schema 选择的命名必须适应所检索实体的类型。由于 GraphQL 的强类型特性,该类型在应用程序层和 API 层可能有所不同。
在这种情况下,虽然在 WordPress 中 "post" 可能意味着任何 "custom post type",但在 GraphQL 中 "post" 必然是 Post。如果字段检索自定义文章,那么 GraphQL schema 中的字段必须命名为 customPosts,而不是 posts。同样,如果输入接收自定义文章的 ID,则必须称为 customPostID,而不是 postID。

这一教训也适用于评论。评论可以添加到任何 CPT,而不仅仅是文章。因此,Comment 类型必须明确这一点,通过包含字段 customPost(而不是 post):
type Comment {
id: ID!
customPost: CustomPostUnion!
}将预定义字符串值转换为 enum,尽可能使用大写
枚举类型(enum)按惯例使用大写定义。例如,graphql.org 的文档提供了以下示例:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}每当我们创建新的 enum 类型时,应该对其定义的常量使用大写。然而,在从应用程序迁移数据模型时,我们可能会遇到某些预定义值的集合,可以通过 enum 映射,但其值是小写字符串。
举例来说,WordPress 中的文章有一个 "status" 属性,包含以下值之一:
"publish""pending""draft""trash"
在 schema 中映射此属性时,Post.status 字段可以返回 String,如下所示:
type Post {
status: String!
}然而,由于状态必然是那些预定义值之一,而不是其他值,我们宁愿将其映射为 enum:
enum Status {
PUBLISH
DRAFT
PENDING
TRASH
}
type Post {
status: Status!
}现在,我们可能会遇到一个问题:enum PUBLISH 在应用程序中将被转换为字符串值 "PUBLISH",而不是 "publish"。
使用大写值而不是预期的小写值,可能会扰乱应用程序中的逻辑。事实上,在 WordPress 中执行以下代码并不起作用:
// This will retrieve all posts, not only the published ones
$published_posts = get_posts([
"post_status" => "PUBLISH",
]);在这种情况下,我们可以考虑在惯例与便利之间进行权衡,仍然使用 enum 来映射常量,但使用小写:
enum Status {
publish
draft
pending
trash
}换句话说,我们可以在规范性和实用性之间找到中间地带。我们在构建 GraphQL schema 时应该使用最佳实践,但在合理的情况下也允许自己偏离它们。