Для людей, которые работают с Headless WordPress, Gato GraphQL предлагает новую замечательную функцию — Headless WordPress без WordPress.

В этом посте объясняется все о ней, также описывается, как это вообще возможно, и показывается видео, демонстрирующее функцию.

​Запуск Gato GraphQL как отдельного PHP-приложения

Gato GraphQL создан с применением автономных компонентов PHP, управляемых через Composer, таким образом, что все компоненты PHP, составляющие сервер GraphQL, не зависят от WordPress!

Таким образом, сервер GraphQL может работать как отдельное PHP-приложение, и вы можете включить его в любое PHP-приложение на основе WordPress или чего-то еще.

Если в каком-то случае вашему приложению не требуется доступ к данным WordPress, то, по крайней мере, вы готовы к работе для такого случая применения.

В этом видео демонстрируется такой случай — взаимодействие с API GitHub для загрузки/установки артефактов из действий GitHub во время разработки.

В видео запрос GraphQL выполняет HTTP-запрос, чтобы получить последние плагины Gato GraphQL, созданные в действиях GitHub, которые загружаются как артефакты при объединении запроса на включение.

URL-адреса артефактов из ответа GraphQL затем вводятся в WP-CLI, чтобы плагины автоматически устанавливались на локальный веб-сервер DEV для запуска тестов.

Подробнее — в последнем разделе.

В этом случае применения, поскольку доступа к данным WordPress вообще нет, сервер GraphQL уже может работать как автономное приложение PHP.

Если бы мне было нужно, я даже смог бы использовать его в своем рабочем процессе GitHub Actions!

​Миграция безголового приложения WordPress

Посмотрим, как запускать данные WordPress без WordPress, когда вы получаете доступ к этим данным.

Схема GraphQL, предоставленная Gato GraphQL, содержит поля для извлечения данных WordPress: сообщений, пользователей, комментариев, тегов, категорий и т. д.

Код в PHP-резолверах, извлекающих данные WordPress, зависит от WordPress; этот код не может работать в приложении, отличном от WordPress.

Однако в Gato GraphQL каждый из этих резолверов написан при помощи двух пакетов. Это:

  1. «Ванильный» PHP, содержащий весь общий код.
  2. WordPress-специфичный код, содержащий фактические вызовы методов WordPress, удовлетворяющих резолверу.

Например, в этом запросе GraphQL:

{
  posts {
    id
    title
  }
}

…логика получения сообщений состоит из:

  1. Поле Root.posts находится в общем пакете posts.
  2. Его разрешение для WordPress при помощи метода get_posts — он находится в специфичном для WordPress пакете posts-wp.

Распределение кода между пакетами, отличными от WordPress, и пакетами WordPress составляет около 80/20 %. Это означает, что 80 % кода можно повторно использовать с другой платформой/CMS и только 20 % кода будет необходимо переопределить.

Более того, вся функциональность Gato GraphQL реализована через модули, и модули можно включать и выключать по желанию.

Модули схемы

Модули — это функция, реализованная в целях безопасности: если вам не нужно раскрывать пользовательские данные в общедоступном API, можно отключить модуль Users — и соответствующие поля (например, Root .users) не будут добавлены в схему.

Модули напрямую сопоставляются с нижележащими пакетами PHP. Таким образом, при запуске Gato GraphQL как автономного приложения можно выборочно загружать только те модули/пакеты, которые нам нужны.

Например, если приложение выводит данные только для сообщений, категорий и тегов, то будут использоваться только пакеты posts-wp, categories-wp и tags-wp вместе с их зависимостями, которые должны быть загружены.

Затем при переходе с WordPress (скажем, на Laravel или Symfony) для новой платформы/CMS потребуется переопределить только эти 3 пакета, специфичные для WordPress.

Как следствие, вы можете использовать WordPress без головы уже сегодня, зная, что в будущем вы сможете перенести свое приложение на другую платформу или CMS с минимальными усилиями.

​Переход на Gato GraphQL с другого API

Если вы уже используете безголовую WordPress, скорее всего, ваше приложение применяет WPGraphQL, либо WP REST API.

К сожалению, с любым из этих двух API вы привязаны к WordPress, то есть за пределами WordPress нет WP REST API, а WPGraphQL без WordPress работать не может.

К счастью, любой из этих API можно заменить на Gato GraphQL и получить возможность перенести свое безголовое приложение WordPress с WordPress [на что-то другое].

Потребуются эти 2 шага:

  1. Переход с WP REST API или WPGraphQL на Gato GraphQL
  2. Переопределение необходимых пакетов, специфичных для WordPress.

Посмотрим, как можно осуществить перенос API.

​WP REST API для устойчивых запросов Gato GraphQL

При помощи расширения Persisted Queries вы можете публиковать REST-подобные конечные точки, составленные при помощи GraphQL.

Для каждой конечной точки REST в вашем приложении можно создать соответствующую конечную точку устойчивого запроса, которая получает те же данные, и использовать эту конечную точку.

Например, следующий запрос GraphQL может заменить конечную точку REST /wp-json/wp/v2/posts/:

{
  posts {
    id
    date: dateStr(format: "Y-m-d\\TH:i:s")
    modified: modifiedDateStr(format: "Y-m-d\\TH:i:s")
    slug
    status
    link: url
    title: self {
      rendered: title
    }
    content: self {
      rendered: content
    },
    excerpt: self {
      rendered: excerpt
    }
    author
    featured_media: featuredImage
    sticky: isSticky
    categories
    tags
  }
}

Благодаря иерархии API устойчивый запрос публикуется по пути /graphql-query/wp/v2/posts/. Это упрощает сопоставление конечных точек.

Чтобы реплицировать конечную точку REST /wp-json/wp/v2/posts/{id}/, которая извлекает данные для сообщения с заданным идентификатором, можно указать идентификатор сообщения в параметре URL-адреса postId.

Например, следующий устойчивый запрос может быть вызван конечной точкой /graphql-query/wp/v2/posts/single/?postId={id}:

query GetPost($postId: ID!) {
  post(by: { id: $postId }) {
    id
    date: dateStr(format: "Y-m-d\\TH:i:s")
    modified: modifiedDateStr(format: "Y-m-d\\TH:i:s")
    slug
    status
    link: url
    title: self {
      rendered: title
    }
    content: self {
      rendered: content
    },
    excerpt: self {
      rendered: excerpt
    }
    author
    featured_media: featuredImage
    sticky: isSticky
    categories
    tags
  }
}

​От WPGraphQL к Gato GraphQL

Схемы GraphQL от WPGraphQL и Gato GraphQL схожи, но немного различаются, поэтому их нужно адаптировать.

Стартер WordPress Next.js leoloso/next-wordpress-starter работает с WPGraphQL или Gato GraphQL. Этот стартер для обоих серверов использует одну и ту же логику JS. Различаются только запросы GraphQL.

В стартере ниже представлено несколько примеров адаптации запросов между двумя серверами. Например, этот запрос WPGraphQL:

fragment PostFields on Post {
  id
  categories {
    edges {
      node {
        databaseId
        id
        name
        slug
      }
    }
  }
  databaseId
  date
  isSticky
  postId
  slug
  title
}

…адаптирован для Gato GraphQL вот так:

fragment PostFields on Post {
  id
  categories: self {
    edges: categories(pagination: { limit: -1 }) {
      node: self {
        databaseId: id
        id
        name
        slug
      }
    }
  }
  databaseId: id
  date: dateStr
  isSticky
  postId: id
  slug
  title
}

​Подробности: запуск Gato GraphQL как безголового приложения PHP

Вот подробное объяснение демо-видео:

Мы предоставляем запрос GraphQL для запуска в файле retrive-github-artifacts.gql.

Запрос подключается к API GitHub, получая токен доступа из переменной среды GITHUB_ACCESS_TOKEN. Он динамически генерирует полный путь к конечной точке actions/artifacts из предоставленных переменных, а затем отправляет к ней HTTP-запрос.

После — из ответа — он извлекает «URL загрузки» из каждого элемента артефакта и отправляет к ним асинхронные HTTP-запросы. Из заголовка Location каждого из этих «URL загрузки» мы получаем фактический URL загружаемого файла.

Наконец, он выводит все URL вместе, разделенные пробелом, чтобы их было удобно скормить WP-CLI.

# Файл retrieve-github-artifacts.gql

query RetrieveProxyArtifactDownloadURLs(
  $repoOwner: String!
  $repoProject: String!
  $perPage: Int = 1
  $artifactName: String = ""
) {
  githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
    @remove

  # Создаем заголовок авторизации для отправки на GitHub.
  authorizationHeader: _sprintf(
    string: "Bearer %s"
    values: [$__githubAccessToken]
  )
    @remove

  # Создаем заголовок авторизации для отправки на GitHub.
  githubRequestHeaders: _echo(
    value: [
      { name: "Accept", value: "application/vnd.github+json" }
      { name: "Authorization", value: $__authorizationHeader }
    ]
  )
    @remove
    @export(as: "githubRequestHeaders")

  githubAPIEndpoint: _sprintf(
    string: "https://api.github.com/repos/%s/%s/actions/artifacts?per_page=%s&name=%s"
    values: [$repoOwner, $repoProject, $perPage, $artifactName]
  )

  # Используем поле из "Send HTTP Request Fields", чтобы соединиться с GitHub
  gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
    input: {
      url: $__githubAPIEndpoint
      options: { headers: $__githubRequestHeaders }
    }
  )
    @remove

  # Наконец, просто извлекаем URL из каждого элемента «артефактов».
  gitHubProxyArtifactDownloadURLs: _objectProperty(
    object: $__gitHubArtifactData
    by: { key: "artifacts" }
  )
    @underEachArrayItem(passValueOnwardsAs: "artifactItem")
      @applyField(
        name: "_objectProperty"
        arguments: { object: $artifactItem, by: { key: "archive_download_url" } }
        setResultInResponse: true
      )
    @export(as: "gitHubProxyArtifactDownloadURLs")
}

query CreateHTTPRequestInputs
  @depends(on: "RetrieveProxyArtifactDownloadURLs")
{
  httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
    @underEachArrayItem(passValueOnwardsAs: "url")
      @applyField(
        name: "_objectAddEntry"
        arguments: {
          object: {
            options: { headers: $githubRequestHeaders, allowRedirects: null }
          }
          key: "url"
          value: $url
        }
        setResultInResponse: true
      )
    @export(as: "httpRequestInputs")
    @remove
}

query RetrieveActualArtifactDownloadURLs
  @depends(on: "CreateHTTPRequestInputs")
{
  _sendHTTPRequests(inputs: $httpRequestInputs) {
    artifactDownloadURL: header(name: "Location")
      @export(as: "artifactDownloadURLs", type: LIST)
  }
}

query PrintSpaceSeparatedArtifactDownloadURLs
  @depends(on: "RetrieveActualArtifactDownloadURLs")
{
  spaceSeparatedArtifactDownloadURLs: _arrayJoin(
    array: $artifactDownloadURLs
    separator: " "
  )
}

Логика PHP напрямую загружает код из плагина Gato GraphQL и из пакета All Extensions (он необходим для отправки HTTP-запросов и другой функциональности).

Так как PHP-приложение автономно, мы должны явно указать, какие модули инициализируются, и предоставить любую конфигурацию, отличную от конфигурации по умолчанию.

Например, скажем модулю SendHTTPRequests разрешить подключение к https://api.github.com/repos, а модулю EnvironmentFields разрешить переменную среды для доступа GITHUB_ACCESS_TOKEN.

Обратите внимание, что схема GraphQL генерируется при первом выполнении запроса GraphQL и кэшируется на диске. Таким образом, начиная со второго раза, не выполняется никакой код вычисления схемы. Это ускоряет выполнение.

Наконец, автономное приложение инициализирует сервер GraphQL, выполняет к нему запрос и выводит ответ.

<?php
// Файл retrieve-github-artifacts.php

declare(strict_types=1);

use GraphQLByPoP\GraphQLServer\Server\StandaloneGraphQLServer;
use PoP\Root\Container\ContainerCacheConfiguration;

//Загрузка сервера GraphQL через автономные компоненты PHP.
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql/vendor/scoper-autoload.php');

// Загрузка расширений PRO через автономные компоненты PHP.
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql-all-extensions-bundle/vendor/scoper-autoload.php');

// Модули, необходимые в запросе GraphQL
$moduleClasses = [
  \PoPSchema\EnvironmentFields\Module::class,
  \PoPSchema\FunctionFields\Module::class,
  \GraphQLByPoP\ExportDirective\Module::class,
  \GraphQLByPoP\DependsOnOperationsDirective\Module::class,
  \GraphQLByPoP\RemoveDirective\Module::class,
  \PoPSchema\ApplyFieldDirective\Module::class,
  \PoPSchema\SendHTTPRequests\Module::class,
  \PoPSchema\ConditionalMetaDirectives\Module::class,
  \PoPSchema\DataIterationMetaDirectives\Module::class,
];

// Конфигурация модулей
$moduleClassConfiguration = [
  \PoP\GraphQLParser\Module::class => [
    \PoP\GraphQLParser\Environment::ENABLE_MULTIPLE_QUERY_EXECUTION => true,
    \PoP\GraphQLParser\Environment::USE_LAST_OPERATION_IN_DOCUMENT_FOR_MULTIPLE_QUERY_EXECUTION_WHEN_OPERATION_NAME_NOT_PROVIDED => true,
    \PoP\GraphQLParser\Environment::ENABLE_RESOLVED_FIELD_VARIABLE_REFERENCES => true,
    \PoP\GraphQLParser\Environment::ENABLE_COMPOSABLE_DIRECTIVES => true,
  ],
  \PoPSchema\SendHTTPRequests\Module::class => [
    \PoPSchema\SendHTTPRequests\Environment::SEND_HTTP_REQUEST_URL_ENTRIES => [
      '#https://api.github.com/repos/(.*)#',
    ],
  ],
  \PoPSchema\EnvironmentFields\Module::class => [
    \PoPSchema\EnvironmentFields\Environment::ENVIRONMENT_VARIABLE_OR_PHP_CONSTANT_ENTRIES => [
      'GITHUB_ACCESS_TOKEN',
    ],
  ],
];

// Кеширование схемы на диск, чтобы ускорить выполнение со второго раза.
$containerCacheConfiguration = new ContainerCacheConfiguration('MyGraphQLServer', true, 'retrieve-github-artifacts', __DIR__ . '/tmp');

// Инициализация сервера
$graphQLServer = new StandaloneGraphQLServer($moduleClasses, $moduleClassConfiguration, [], [], $containerCacheConfiguration);

/**
 * Запрос GraphQL для выполнения, хранящийся в собственном файле .gql.
 *
 * @var string
 */
$query = file_get_contents(__DIR__ . '/retrieve-github-artifacts.gql');

// Переменные GraphQL
$variables = [
  'repoOwner' => 'GatoGraphQL',
  'repoProject' => 'GatoGraphQL',
  'perPage' => 3
];

// Выполнение запроса
$response = $graphQLServer->execute(
  $query,
  $variables,
);

// Вывод запроса
echo $response->getContent();

Чтобы выполнить запрос GraphQL, запустим файл в терминале, используя jq для красивой печати вывода JSON:

php retrieve-github-artifacts.php | jq

Наконец, чтобы извлечь URL-адреса артефактов из ответа GraphQL и скормить их WP-CLI, запускаем:

GITHUB_ARTIFACT_URLS=$(php retrieve-github-artifacts.php \
  | grep -E -o '"spaceSeparatedArtifactDownloadURLs\":"(.*)"' \
  | cut -d':' -f2- | cut -d'"' -f2- | rev | cut -d'"' -f2- | rev \
  | sed 's/\\\//\//g')
wp plugin install ${GITHUB_ARTIFACT_URLS} --force --activate

Как показано в видео, мы можем запустить Gato GraphQL без WordPress.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Leonardo Losoviz: Introducing: Headless WordPress without WordPress

Предыдущая статьяЯзык запросов Lisp Query Notation
Следующая статьяC++: руководство по сортировке строк