Блог

🦸🏻‍♂️ Представляем: Headless WordPress без WordPress

Leonardo Losoviz
Автор: Leonardo Losoviz ·

С момента скандала Matt Mullenweg против WPEngine я замечаю всё больше людей на Reddit (и в других местах), которые спрашивают об альтернативах WordPress — не обязательно для того, чтобы немедленно уйти с WordPress, а чтобы понять, какие варианты у них есть и насколько болезненной может оказаться потенциальная миграция. Они хотят знать, как подстраховаться.

Для тех, кто работает с 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 Actions в процессе разработки:

Демо Headless WordPress без WordPress: выполнение GraphQL query

В видео GraphQL query выполняет HTTP-запрос для получения последних плагинов Gato GraphQL, сгенерированных в GitHub Actions, которые загружаются как артефакты при слиянии pull request.

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

(Подробнее об этом я расскажу в последнем разделе этой записи.)

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

При необходимости я даже мог бы использовать его внутри своего workflow GitHub Actions!

Миграция headless WordPress-приложения

Когда вам всё же нужно обращаться к данным WordPress, давайте посмотрим, как запустить это без WordPress.

Схема GraphQL, предоставляемая Gato GraphQL, содержит поля для получения данных WordPress: posts, users, comments, tags, categories и т.д.

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

Однако в Gato GraphQL каждый из этих резолверов реализован через 2 пакета:

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

Например, для следующего GraphQL query:

{
  posts {
    id
    title
  }
}

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

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

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

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

Модули схемы
Модули схемы

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

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

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

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

Следовательно, вы можете использовать headless WordPress уже сегодня, зная, что в дальнейшем сможете перенести своё приложение на другой фреймворк или CMS с минимальными усилиями.

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

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

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

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

Для этого потребуются 2 шага:

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

Давайте посмотрим, как можно осуществить переход с API.

WP REST API на persisted queries Gato GraphQL

С помощью расширения Persisted queries вы можете публиковать REST-подобные эндпоинты, составленные с использованием GraphQL.

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

Например, следующий GraphQL query может заменить 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 persisted query можно опубликовать по пути /graphql-query/wp/v2/posts/, что упрощает сопоставление эндпоинтов.

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

Например, следующий persisted query можно вызвать по эндпоинту /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 схожи, но немного отличаются, поэтому их нужно адаптировать.

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

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

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 query для выполнения в файле retrieve-github-artifacts.gql.

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

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

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

# File retrieve-github-artifacts.gql
 
query RetrieveProxyArtifactDownloadURLs(
  $repoOwner: String!
  $repoProject: String!
  $perPage: Int = 1
  $artifactName: String = ""
) {
  githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
    @remove
 
  # Create the authorization header to send to GitHub
  authorizationHeader: _sprintf(
    string: "Bearer %s"
    values: [$__githubAccessToken]
  )
    @remove
 
  # Create the authorization header to send to 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]
  )
 
  # Use the field from "Send HTTP Request Fields" to connect to GitHub
  gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
    input: {
      url: $__githubAPIEndpoint
      options: { headers: $__githubRequestHeaders }
    }
  )
    @remove
 
  # Finally just extract the URL from within each "artifacts" item
  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 и из бандла «Power Extensions» (необходимого для отправки HTTP-запросов и другого функционала).

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

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

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

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

<?php
// File retrieve-github-artifacts.php
 
declare(strict_types=1);
 
use GraphQLByPoP\GraphQLServer\Server\StandaloneGraphQLServer;
use PoP\Root\Container\ContainerCacheConfiguration;
 
// Load the GraphQL server via the standalone PHP components
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql/vendor/scoper-autoload.php');
 
// Load the PRO extensions via the standalone PHP components
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql-power-extensions-bundle/vendor/scoper-autoload.php');
 
// Modules required in the GraphQL query
$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,
];
 
// Configure the modules
$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',
    ],
  ],
];
 
// Cache the schema to disk, to speed-up execution from the 2nd time onwards
$containerCacheConfiguration = new ContainerCacheConfiguration('MyGraphQLServer', true, 'retrieve-github-artifacts', __DIR__ . '/tmp');
 
// Initialize the server
$graphQLServer = new StandaloneGraphQLServer($moduleClasses, $moduleClassConfiguration, [], [], $containerCacheConfiguration);
 
/**
 * GraphQL query to execute, stored in its own .gql file
 *
 * @var string
 */
$query = file_get_contents(__DIR__ . '/retrieve-github-artifacts.gql');
 
// GraphQL variables
$variables = [
  'repoOwner' => 'GatoGraphQL',
  'repoProject' => 'GatoGraphQL',
  'perPage' => 3
];
 
// Execute the query
$response = $graphQLServer->execute(
  $query,
  $variables,
);
 
// Print the response
echo $response->getContent();

Чтобы выполнить GraphQL query, запускаем в терминале (используя 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.


Подпишитесь на нашу рассылку

Будьте в курсе всех обновлений Gato GraphQL.