解析器、变更与订阅
通过解析器获取和修改数据
解析器、变更与订阅 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。
解析器承载业务逻辑
模式描述存在什么;解析器决定每个字段的值如何产生。解析器就是一个可调用对象。变更是会修改状态的解析器。订阅会随着时间推送值。本课将介绍这三者,以及将它们联系起来的执行模型。
解析器签名
每个解析器都会接收四个参数:($objectValue, $args, $context, ResolveInfo $info)。
- $objectValue——父级已经解析出的值(顶层则为
rootValue)。 - $args——字段的参数。
- $context——每个请求共享的状态(数据库句柄、当前用户)。
- $info——AST/字段元数据(字段名称、选择集、路径)。
<?php
use GraphQL\Type\Definition\ResolveInfo;
$resolve = function ($objectValue, array $args, $context, ResolveInfo $info) {
// $context['db'], $context['user'] set up per request
return $context['db']->find($args['id']);
};
默认解析器
如果您没有提供 resolve,graphql-php 的默认解析器会从父级值中读取字段名称:可以是数组键、公共属性,或 get<Field>() 方法。这意味着您通常只需从父级返回普通数组或数据传输对象,就能以零样板代码解析整个对象类型。
<?php
// Parent returns this array; child fields resolve by key automatically:
$user = [
'id' => 1,
'name' => 'Ada',
'email' => 'ada@example.com',
];
// 'name' field -> $user['name'] with no explicit resolver needed
var_dump($user['name']);
解析器从父级级联到子级
执行过程自顶向下:Query.user 解析器返回一个用户,它会成为 User.posts 的 $objectValue;后者的结果又会成为每个 Post.title 的父级。理解这种级联非常重要——N+1 问题正是在这里出现的(下一课将介绍)。
<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
$userType = new ObjectType([
'name' => 'User',
'fields' => fn() => [
'id' => Type::id(),
'name' => Type::string(),
'posts' => [
'type' => Type::listOf(Type::string()),
// $user is the parent value resolved by Query.user
'resolve' => fn($user) => Posts::titlesForUser($user['id']),
],
],
]);
返回异步结果
解析器可以返回一个值或一个延迟结果。graphql-php 提供同步的异步结果适配器;使用 ReactPHP/Amp 适配器可以延迟解析并进行批处理。即使采用同步方式,返回 Deferred 对象也能让执行器收集任务,并在当前解析层级结束后运行它们——DataLoader 正是建立在这一机制之上。
<?php
use GraphQL\Deferred;
$resolve = function ($post) use ($authorBuffer) {
$authorBuffer->add($post['author_id']); // queue the id
return new Deferred(function () use ($authorBuffer, $post) {
$authorBuffer->loadOnce(); // one batched query
return $authorBuffer->get($post['author_id']);
});
};
变更会修改状态
变更其实就是一个名为 Mutation 的根类型。按照约定,它的顶层字段会按顺序运行(而不是并行运行),以保证副作用有序。输入通常会归 agrup into? an InputObjectType for a clean signature.
<?php
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\Type;
$createPostInput = new InputObjectType([
'name' => 'CreatePostInput',
'fields' => [
'title' => Type::nonNull(Type::string()),
'body' => Type::string(),
],
]);
连接变更类型
变更字段将输入对象作为参数,并返回已创建的实体(这样客户端就能在同一轮请求中读取字段)。请在解析器内部执行验证和授权,失败时抛出异常。
<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
$mutationType = new ObjectType([
'name' => 'Mutation',
'fields' => [
'createPost' => [
'type' => $postType,
'args' => ['input' => Type::nonNull($createPostInput)],
'resolve' => function ($root, array $args, $context) {
if (!$context['user']) {
throw new \RuntimeException('Unauthenticated');
}
return PostRepo::create($args['input'], $context['user']);
},
],
],
]);
错误:对客户端安全与内部错误
默认情况下,graphql-php 会隐藏异常消息,并显示 Internal server error,以避免泄露内部信息。要向客户端显示消息,请实现 GraphQL\Error\ClientAware,并从 isClientSafe() 返回 true。您还可以通过 extensions 添加机器可读的错误代码。
<?php
use GraphQL\Error\ClientAware;
class ValidationError extends \RuntimeException implements ClientAware {
public function isClientSafe(): bool { return true; }
// older versions also used getCategory(): string
}
订阅:概念
订阅根类型允许客户端在事件发生时接收结果流(新消息、价格变动)。GraphQL 规范定义了订阅的语义,但 graphql-php 每次调用只执行一个操作——它本身不会运行长连接套接字服务器。传输层需要由您提供。
- graphql-php 会为您推送的每个事件解析订阅的负载。
- 传输层(通过 Ratchet/Mercure/Pusher 建立的 WebSocket)会将事件交付给客户端。
订阅解析器的结构
实践中,您会将订阅拆分为订阅步骤(登记关注,返回事件源)和解析步骤(将每个事件映射为字段的负载)。许多 PHP 技术栈会将 graphql-php 与 Mercure 或发布/订阅代理结合使用;下面的解析器展示了由 graphql-php 负责的逐事件映射。
<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
$subscriptionType = new ObjectType([
'name' => 'Subscription',
'fields' => [
'messageAdded' => [
'type' => $messageType,
'args' => ['channelId' => Type::nonNull(Type::id())],
// graphql-php resolves each pushed event into the payload;
// a WebSocket/Mercure transport drives when this runs.
'resolve' => fn($event) => $event['message'],
],
],
]);
上下文是授权与 DI 通道
第三个解析器参数 $context 会在每个请求开始时构建一次,然后传递给每个解析器。它适合存放已通过身份验证的用户、数据库连接和您的 DataLoaders。在这里集中处理授权可以让解析器保持简洁——它们只需向上下文询问用户身份,而不必重新推导。
<?php
require 'vendor/autoload.php';
// Built once per HTTP request, passed to executeQuery():
$context = [
'user' => authenticate($_SERVER['HTTP_AUTHORIZATION'] ?? ''),
'db' => $pdo,
];
$resolve = function ($root, array $args, array $context) {
if ($context['user'] === null) {
throw new \RuntimeException('Unauthenticated');
}
return $context['db']->find($args['id']);
};
快速检查
如何让 GraphQL 客户端看到异常消息?
总结
您了解了 GraphQL 的执行核心:
- 解析器接收
($value, $args, $context, $info);默认解析器从父级读取键/获取方法。 - 解析过程从父级级联到子级——这正是 N+1 的来源。
- 返回
Deferred/异步结果可以实现批处理。 - 变更是使用
InputObjectType的顺序执行根字段;请在解析器中执行授权/验证。 - 订阅定义负载解析,而传输层由您提供。
ClientAware控制客户端可以看到哪些错误消息。
下一步:使用 DataLoader 消除 N+1 问题。
常见问题解答
「解析器、变更与订阅」课时是免费的吗?
是的 — 「解析器、变更与订阅」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。
「解析器、变更与订阅」这节课中我会学到什么?
通过解析器获取和修改数据 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PHP Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「解析器、变更与订阅」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PHP Academy 课中编写并运行代码吗?
能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。