0Pricing
PHP Academy · Урок

Резолверы, мутации и подписки

Получайте и изменяйте данные через резолверы

«Резолверы, мутации и подписки» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. GraphQL и REST
  2. Создание схемы с graphql-php
  3. Резолверы, мутации и подписки
  4. Производительность: N+1 и DataLoader
← Назад к PHP Academy