PHP `GraphQL` API 设计:类型系统、解析器与数据加载器 (DataLoader)

各位同学,大家好!我是今天的主讲人,咱们今天就来聊聊如何在 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 类型系统定义的格式。每个字段都需要一个解析器。

解析器函数接收四个参数:

  1. $rootValue: 父对象的值。如果是根查询,则为 null
  2. $args: 客户端传递的参数。
  3. $context: 上下文对象,包含请求相关的信息,例如用户身份验证信息。
  4. $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 类型,它有两个字段:userusers

  • 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 类型,它有两个字段:createUserupdateUser

  • 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。如果我们查询多个 Postauthor 信息,就会导致 N+1 次数据库查询 (1 次查询所有 Post,N 次查询每个 Postauthor)。

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 需要深入理解这三个概念,并根据实际情况进行灵活应用。

希望今天的讲解对大家有所帮助!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注