Резолверы, мутации и подписки
Получайте и изменяйте данные через резолверы
«Резолверы, мутации и подписки» — бесплатный урок PHP Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения 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 возвращает пользователя, который становится $objectValue для User.posts, а результат этого поля становится родительским значением для каждого 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 — это всего лишь корневой тип с именем Mutation. По соглашению его поля верхнего уровня выполняются последовательно, а не параллельно, поэтому побочные эффекты происходят в заданном порядке. Входные данные обычно объединяют в InputObjectType для удобной сигнатуры.
<?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(),
],
]);
Подключение типа Mutation
Поле мутации принимает входной объект как аргумент и возвращает созданную сущность, чтобы клиенты могли прочитать её поля в рамках того же обмена данными. Выполняйте проверку и авторизацию внутри резолвера, выбрасывая исключение при сбое.
<?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 и возвращайте true из isClientSafe(). Добавляйте машиночитаемые коды через extensions.
<?php
use GraphQL\Error\ClientAware;
class ValidationError extends \RuntimeException implements ClientAware {
public function isClientSafe(): bool { return true; }
// older versions also used getCategory(): string
}
Подписки: концепция
Корневой тип Subscription позволяет клиентам получать поток результатов при возникновении событий (новое сообщение, изменение цены). Спецификация GraphQL определяет семантику подписок, но graphql-php выполняет одну операцию за вызов — сама библиотека не запускает долгоживущий сервер сокетов. Транспорт Вы предоставляете самостоятельно.
- graphql-php разрешает полезную нагрузку подписки для каждого отправленного Вами события.
- Транспорт (WebSocket через Ratchet/Mercure/Pusher) доставляет события клиентам.
Структура резолвера подписки
На практике подписку разделяют на шаг подписки (регистрация интереса, возвращающая источник событий) и шаг разрешения (отображение каждого события на полезную нагрузку поля). Многие стеки 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управляет тем, какие сообщения об ошибках могут видеть клиенты.
Далее: устраняем проблему N+1 с помощью DataLoader.
Часто задаваемые вопросы
Урок «Резолверы, мутации и подписки» бесплатный?
Да — полный текст урока «Резолверы, мутации и подписки» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.
Чему я научусь в уроке «Резолверы, мутации и подписки»?
Получайте и изменяйте данные через резолверы Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать PHP Academy?
Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Резолверы, мутации и подписки»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке PHP Academy?
Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- GraphQL и REST
- Создание схемы с graphql-php
- Резолверы, мутации и подписки
- Производительность: N+1 и DataLoader