各位同学,大家好!我是今天的主讲人,咱们今天就来聊聊如何在 PHP 中构建一个强大的 GraphQL API。我会尽量用大白话,让大家都能听明白。
GraphQL 是一种查询语言,它允许客户端精确地请求所需的数据,而不是像 REST API 那样一股脑地返回所有信息。这不仅提高了效率,也降低了网络带宽的消耗。让我们一起深入 PHP 的 GraphQL 世界!
第一部分:GraphQL 类型系统:给数据定规矩
GraphQL 的核心是类型系统。类型系统就像给数据穿上衣服,告诉 GraphQL 如何理解和处理数据。它定义了数据结构,确保数据的一致性和有效性。
先来看几个基本类型:
Int
: 整数Float
: 浮点数String
: 字符串Boolean
: 布尔值ID
: 唯一标识符 (通常是字符串)
除了这些基本类型,我们还可以自定义类型。比如,我们要定义一个 User
类型:
use GraphQLTypeDefinitionObjectType;
use GraphQLTypeDefinitionType;
$userType = new ObjectType([
'name' => 'User',
'description' => '用户信息',
'fields' => [
'id' => [
'type' => Type::nonNull(Type::id()),
'description' => '用户ID',
],
'name' => [
'type' => Type::string(),
'description' => '用户名',
],
'email' => [
'type' => Type::string(),
'description' => '用户邮箱',
],
'age' => [
'type' => Type::int(),
'description' => '用户年龄',
],
],
]);
这段代码定义了一个名为 User
的对象类型。它有四个字段:id
(ID 类型,不能为空), name
(字符串类型), email
(字符串类型) 和 age
(整数类型)。 Type::nonNull()
表示该字段不能为空。
类型修饰符:让类型更灵活
GraphQL 还有一些类型修饰符,让类型系统更加强大:
nonNull(Type $type)
: 表示该类型不能为空,必须有值。listOf(Type $type)
: 表示该类型是一个列表,包含多个相同类型的元素。
例如,如果一个用户可以有多个角色,我们可以这样定义 roles
字段:
use GraphQLTypeDefinitionListOfType;
'roles' => [
'type' => Type::listOf(Type::string()), // 角色列表,每个角色都是字符串
'description' => '用户角色列表',
],
枚举类型 (Enum):限定取值范围
枚举类型允许我们定义一个字段只能取预定义的值。例如,我们可以定义一个 UserStatus
枚举类型:
use GraphQLTypeDefinitionEnumType;
$userStatusType = new EnumType([
'name' => 'UserStatus',
'description' => '用户状态',
'values' => [
'ACTIVE' => [
'value' => 'active',
'description' => '活跃',
],
'INACTIVE' => [
'value' => 'inactive',
'description' => '不活跃',
],
'PENDING' => [
'value' => 'pending',
'description' => '待验证',
],
],
]);
这样,User
类型的 status
字段就可以使用 UserStatus
类型:
'status' => [
'type' => $userStatusType,
'description' => '用户状态',
],
输入类型 (Input Object):传递复杂参数
输入类型允许我们定义一个对象类型作为参数传递给 GraphQL 查询或变更 (Mutation)。例如,我们可以定义一个 UserInput
类型:
use GraphQLTypeDefinitionInputObjectType;
$userInputType = new InputObjectType([
'name' => 'UserInput',
'description' => '创建/更新用户的信息',
'fields' => [
'name' => [
'type' => Type::nonNull(Type::string()),
'description' => '用户名',
],
'email' => [
'type' => Type::nonNull(Type::string()),
'description' => '用户邮箱',
],
'age' => [
'type' => Type::int(),
'description' => '用户年龄',
],
],
]);
接口 (Interface):定义共同行为
接口定义了一组字段,不同的类型可以实现这个接口,从而保证它们具有相同的行为。 例如,我们可以定义一个 Node
接口,包含一个 id
字段:
use GraphQLTypeDefinitionInterfaceType;
use GraphQLTypeDefinitionType;
$nodeInterface = new InterfaceType([
'name' => 'Node',
'description' => '所有节点的接口',
'fields' => [
'id' => [
'type' => Type::nonNull(Type::id()),
'description' => '节点ID',
],
],
'resolveType' => function ($value) use ($userType) {
// 根据 $value 的类型返回对应的 ObjectType
if ($value instanceof User) { // 假设 User 类存在
return $userType;
}
return null; // 如果无法确定类型,返回 null
},
]);
然后,User
类型可以实现 Node
接口:
$userType = new ObjectType([
'name' => 'User',
'description' => '用户信息',
'fields' => [
// ... 其他字段 ...
],
'interfaces' => [$nodeInterface], // 实现 Node 接口
]);
注意,实现接口的类型必须包含接口中定义的所有字段。 resolveType
函数用于在查询接口时确定返回的具体类型。
联合类型 (Union):多种类型返回
联合类型允许一个字段返回多种不同的类型。与接口不同的是,联合类型的成员之间没有共同的字段。 例如,我们可以定义一个 SearchResult
联合类型:
use GraphQLTypeDefinitionUnionType;
$searchResultType = new UnionType([
'name' => 'SearchResult',
'description' => '搜索结果',
'types' => [$userType, $articleType], // 假设 articleType 已定义
'resolveType' => function ($value) use ($userType, $articleType) {
// 根据 $value 的类型返回对应的 ObjectType
if ($value instanceof User) { // 假设 User 类存在
return $userType;
} elseif ($value instanceof Article) { // 假设 Article 类存在
return $articleType;
}
return null; // 如果无法确定类型,返回 null
},
]);
第二部分:解析器 (Resolvers):数据从哪里来?
解析器是 GraphQL API 的大脑。它们负责从数据源获取数据,并将数据转换为 GraphQL 类型系统定义的格式。每个字段都需要一个解析器。
解析器函数接收四个参数:
$rootValue
: 父对象的值。如果是根查询,则为null
。$args
: 客户端传递的参数。$context
: 上下文对象,包含请求相关的信息,例如用户身份验证信息。$info
: 查询信息,包含查询的 AST (Abstract Syntax Tree)。
让我们为 User
类型的 name
字段定义一个解析器:
$userType = new ObjectType([
'name' => 'User',
'description' => '用户信息',
'fields' => [
'id' => [
'type' => Type::nonNull(Type::id()),
'description' => '用户ID',
],
'name' => [
'type' => Type::string(),
'description' => '用户名',
'resolve' => function ($rootValue, $args, $context, $info) {
return $rootValue['name']; // 从 $rootValue 中获取 name 字段的值
},
],
// ... 其他字段 ...
],
]);
在这个例子中,解析器函数从 $rootValue
数组中获取 name
字段的值。 $rootValue
通常是由父字段的解析器提供的。
根查询类型 (Query):GraphQL 的入口
根查询类型定义了 GraphQL API 的入口点。它指定了客户端可以查询哪些数据。
use GraphQLTypeDefinitionObjectType;
use GraphQLTypeDefinitionType;
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'user' => [
'type' => $userType,
'args' => [
'id' => ['type' => Type::nonNull(Type::id())],
],
'resolve' => function ($rootValue, $args, $context, $info) {
// 从数据库或其他数据源获取用户信息
// 这里只是一个示例,实际情况需要根据你的数据源进行调整
$userId = $args['id'];
// 假设我们有一个 User 类,并且可以通过 ID 获取用户信息
$user = User::find($userId);
if (!$user) {
return null; // 用户不存在
}
return $user; // 返回 User 对象
},
],
'users' => [
'type' => Type::listOf($userType),
'resolve' => function ($rootValue, $args, $context, $info) {
// 从数据库或其他数据源获取所有用户信息
// 这里只是一个示例,实际情况需要根据你的数据源进行调整
// 假设我们有一个 User 类,并且可以获取所有用户信息
$users = User::all();
return $users; // 返回 User 对象数组
},
],
],
]);
这段代码定义了一个 Query
类型,它有两个字段:user
和 users
。
user
字段接收一个id
参数,返回一个User
对象。users
字段返回一个User
对象列表。
变更类型 (Mutation):修改数据
变更类型用于修改数据。它类似于 REST API 中的 POST、PUT、DELETE 等方法。
use GraphQLTypeDefinitionObjectType;
use GraphQLTypeDefinitionType;
$mutationType = new ObjectType([
'name' => 'Mutation',
'fields' => [
'createUser' => [
'type' => $userType,
'args' => [
'input' => ['type' => Type::nonNull($userInputType)],
],
'resolve' => function ($rootValue, $args, $context, $info) {
// 创建用户
$input = $args['input'];
// 假设我们有一个 User 类,并且可以创建用户
$user = User::create([
'name' => $input['name'],
'email' => $input['email'],
'age' => $input['age'],
]);
return $user; // 返回新创建的 User 对象
},
],
'updateUser' => [
'type' => $userType,
'args' => [
'id' => ['type' => Type::nonNull(Type::id())],
'input' => ['type' => Type::nonNull($userInputType)],
],
'resolve' => function ($rootValue, $args, $context, $info) {
// 更新用户
$id = $args['id'];
$input = $args['input'];
// 假设我们有一个 User 类,并且可以通过 ID 更新用户信息
$user = User::find($id);
if (!$user) {
return null; // 用户不存在
}
$user->update([
'name' => $input['name'],
'email' => $input['email'],
'age' => $input['age'],
]);
return $user; // 返回更新后的 User 对象
},
],
],
]);
这段代码定义了一个 Mutation
类型,它有两个字段:createUser
和 updateUser
。
createUser
字段接收一个input
参数 (类型为UserInput
),创建一个新的User
对象。updateUser
字段接收一个id
参数和一个input
参数 (类型为UserInput
),更新一个已存在的User
对象。
Schema:将所有类型组合在一起
Schema 是 GraphQL API 的蓝图。它将所有类型 (包括根查询类型和变更类型) 组合在一起,定义了 API 的整体结构。
use GraphQLTypeSchema;
$schema = new Schema([
'query' => $queryType,
'mutation' => $mutationType,
]);
执行 GraphQL 查询
有了 Schema,我们就可以执行 GraphQL 查询了。
use GraphQLGraphQL;
try {
$query = '{
user(id: 1) {
id
name
email
age
}
}';
$result = GraphQL::executeQuery($schema, $query);
$output = $result->toArray();
} catch (Exception $e) {
$output = [
'errors' => [
['message' => $e->getMessage()]
]
];
}
header('Content-Type: application/json');
echo json_encode($output);
这段代码执行了一个 GraphQL 查询,获取 id
为 1 的用户信息。
第三部分:数据加载器 (DataLoader):解决 N+1 问题
在 GraphQL API 中,经常会遇到 N+1 查询问题。假设我们有一个 Post
类型,每个 Post
都有一个 author
字段,类型为 User
。如果我们查询多个 Post
的 author
信息,就会导致 N+1 次数据库查询 (1 次查询所有 Post
,N 次查询每个 Post
的 author
)。
DataLoader 可以有效地解决这个问题。DataLoader 会将多个请求合并成一个批量请求,从而减少数据库查询的次数。
use OverblogDataLoaderDataLoader;
use OverblogDataLoaderPromiseAdapterWebonyxGraphQLSyncPromiseAdapter;
use GraphQLExecutorPromisePromiseAdapter;
// 创建一个 DataLoader 实例
$userLoader = new DataLoader(
function (array $ids) {
// 从数据库或其他数据源批量获取用户信息
// 这里只是一个示例,实际情况需要根据你的数据源进行调整
$users = User::whereIn('id', $ids)->get();
// DataLoader 需要返回一个与 $ids 顺序相同的数组
$usersById = [];
foreach ($users as $user) {
$usersById[$user->id] = $user;
}
$result = [];
foreach ($ids as $id) {
$result[] = isset($usersById[$id]) ? $usersById[$id] : null;
}
return $result;
},
new WebonyxGraphQLSyncPromiseAdapter(new GraphQLExecutorPromiseAdapterSyncPromise())
);
// 在上下文中传递 DataLoader 实例
$context = [
'userLoader' => $userLoader,
];
然后,在 Post
类型的 author
字段的解析器中使用 DataLoader:
$postType = new ObjectType([
'name' => 'Post',
'fields' => [
'id' => [
'type' => Type::nonNull(Type::id()),
],
'title' => [
'type' => Type::string(),
],
'author' => [
'type' => $userType,
'resolve' => function ($rootValue, $args, $context, $info) {
// 使用 DataLoader 加载用户信息
$userId = $rootValue['author_id'];
return $context['userLoader']->load($userId);
},
],
],
]);
DataLoader 的 load()
方法会返回一个 Promise 对象。当 GraphQL 执行引擎需要获取 author
字段的值时,它会等待 Promise 对象 resolve。DataLoader 会将多个 load()
请求合并成一个批量请求,并在批量请求完成后 resolve 所有 Promise 对象。
总结
今天我们学习了 GraphQL API 的三个核心概念:类型系统、解析器和数据加载器。类型系统定义了数据的结构,解析器负责从数据源获取数据,数据加载器解决了 N+1 查询问题。
概念 | 描述 |
---|---|
类型系统 | 定义 GraphQL API 的数据结构,确保数据的一致性和有效性。包括基本类型 (Int, Float, String, Boolean, ID)、自定义类型 (ObjectType, EnumType, InputObjectType, InterfaceType, UnionType) 和类型修饰符 (nonNull, listOf)。 |
解析器 | 负责从数据源获取数据,并将数据转换为 GraphQL 类型系统定义的格式。每个字段都需要一个解析器。解析器函数接收四个参数:$rootValue , $args , $context 和 $info 。 |
数据加载器 | 用于解决 GraphQL API 中的 N+1 查询问题。DataLoader 会将多个请求合并成一个批量请求,从而减少数据库查询的次数。DataLoader 的 load() 方法会返回一个 Promise 对象。 |
记住,构建一个强大的 GraphQL API 需要深入理解这三个概念,并根据实际情况进行灵活应用。
希望今天的讲解对大家有所帮助!下次再见!