Blog

🦸🏻‍♂️ Apresentando: Headless WordPress sem WordPress

Leonardo Losoviz
Por Leonardo Losoviz ·

Desde a polêmica entre Matt Mullenweg e WPEngine, tenho notado cada vez mais pessoas no Reddit (e em outros lugares) perguntando sobre alternativas ao WordPress — não necessariamente para abandonar o WordPress (pelo menos não imediatamente), mas para entender quais opções têm e o quanto seria dolorosa uma eventual migração. Elas querem saber como se precaver.

Para quem trabalha com headless WordPress, o Gato GraphQL agora oferece uma nova e interessante funcionalidade: Headless WordPress sem WordPress.

Este artigo explica tudo sobre isso, descrevendo como isso é possível e apresentando um vídeo de demonstração.

Executando o Gato GraphQL como uma aplicação PHP standalone

O Gato GraphQL foi construído utilizando componentes PHP standalone, gerenciados via Composer, de tal forma que todos os componentes PHP que compõem o servidor GraphQL não dependem do WordPress!

Dessa forma, o servidor GraphQL pode ser executado como uma aplicação PHP standalone, e você pode incluí-lo em qualquer aplicação PHP, baseada em WordPress ou em qualquer outra tecnologia.

Se para algum caso de uso sua aplicação não precisa acessar dados do WordPress, então, ao menos para esse caso de uso, você já está pronto.

Este vídeo demonstra um caso de uso desse tipo: interagir com a API do GitHub para baixar/instalar artefatos do GitHub Actions durante o desenvolvimento:

Demo Headless WordPress sem WordPress: executando uma query GraphQL

No vídeo, a query GraphQL executa uma requisição HTTP para buscar os últimos plugins do Gato GraphQL gerados no GitHub Actions, que são carregados como artefatos ao fazer merge de um pull request.

As URLs dos artefatos presentes na resposta GraphQL são então injetadas no WP-CLI, para que os plugins sejam instalados automaticamente em um servidor web DEV local para executar os testes.

(Vou explicar com mais detalhes na última seção deste artigo.)

Neste caso de uso, como nenhum dado do WordPress é acessado, o servidor GraphQL já pode ser executado como uma aplicação PHP standalone.

Se precisasse, eu poderia até mesmo utilizá-lo dentro do meu workflow do GitHub Actions!

Migrando uma aplicação headless WordPress

Sempre que você acessa de fato dados do WordPress, vejamos como executar isso sem o WordPress.

O schema GraphQL fornecido pelo Gato GraphQL contém campos para buscar dados do WordPress: posts, users, comments, tags, categories, etc.

O código nos resolvers PHP que busca dados do WordPress depende do WordPress; esse código não pode ser executado em uma aplicação não-WordPress.

No entanto, o Gato GraphQL tem cada um desses resolvers implementado via 2 pacotes:

  1. Um PHP "vanilla", contendo todo o código genérico
  2. Um específico para WordPress, contendo as invocações reais aos métodos do WordPress que satisfazem aquele resolver

Por exemplo, nesta query GraphQL:

{
  posts {
    id
    title
  }
}

...a lógica para buscar posts é composta por:

  1. O campo Root.posts: vive no pacote posts genérico
  2. Sua resolução para WordPress via o método get_posts: vive no pacote posts-wp específico para WordPress.

A divisão do código entre os pacotes não-WordPress/WordPress é de aproximadamente 80/20%, o que significa que 80% do código é reutilizável com outro framework/CMS, e apenas 20% do código precisaria ser reimplementado.

Além disso, toda a funcionalidade do Gato GraphQL é distribuída via módulos, e os módulos podem ser habilitados/desabilitados conforme necessário.

Módulos do schema
Módulos do schema

Modules é uma funcionalidade implementada para fins de segurança: se você não precisar expor dados de usuários na sua API pública, pode desabilitar o módulo Users, e os campos correspondentes (como Root.users) nunca serão adicionados ao schema.

Os módulos são diretamente mapeados para os pacotes PHP subjacentes. Assim, ao executar o Gato GraphQL como uma aplicação standalone, podemos carregar seletivamente os módulos/pacotes de que precisamos, e nenhum outro.

Por exemplo, se sua aplicação apenas exibe dados de posts, categorias e tags, então somente os pacotes posts-wp, categories-wp e tags-wp (junto com suas dependências) precisam ser carregados.

Em seguida, ao migrar para longe do WordPress (digamos, para Laravel ou Symfony), apenas esses 3 pacotes específicos do WordPress precisariam ser reimplementados para o novo framework/CMS, e nada mais.

Consequentemente, você pode usar headless WordPress hoje, sabendo que, no futuro, poderá migrar sua aplicação para outro framework ou CMS com esforço mínimo.

Transitando para o Gato GraphQL a partir de outra API

Se você já faz headless WordPress, é provável que sua aplicação utilize a WP REST API ou o WPGraphQL.

Infelizmente, com qualquer uma dessas duas APIs você está vinculado ao WordPress: não existe uma WP REST API fora do WordPress, e o WPGraphQL não pode ser executado sem WordPress.

Felizmente, é possível substituir qualquer uma delas pelo Gato GraphQL e obter a capacidade de migrar sua aplicação headless WordPress para fora do WordPress.

Esses 2 passos seriam necessários:

  1. Transitar da WP REST API ou WPGraphQL para o Gato GraphQL
  2. Reimplementar os pacotes específicos do WordPress necessários

Vejamos como a transição de API pode ser feita.

WP REST API para as queries persistidas do Gato GraphQL

Com a extensão Persisted Queries você pode publicar endpoints semelhantes a REST, compostos usando GraphQL.

Para cada um dos endpoints REST da sua aplicação, você pode criar um endpoint de query persistida correspondente que recupera os mesmos dados, e usar esse endpoint em seu lugar.

Por exemplo, a seguinte query GraphQL pode substituir o endpoint 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
  }
}

Graças à hierarquia de API, a query persistida pode ser publicada sob o caminho /graphql-query/wp/v2/posts/, facilitando o mapeamento dos endpoints.

Para replicar o endpoint REST /wp-json/wp/v2/posts/{id}/, que recupera os dados do post com o ID fornecido, podemos fornecer o ID do post pelo parâmetro de URL postId.

Por exemplo, a seguinte query persistida pode ser invocada pelo endpoint /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 para Gato GraphQL

O schema GraphQL do WPGraphQL e o do Gato GraphQL são semelhantes, mas ligeiramente diferentes, então precisam ser adaptados.

O starter WordPress com Next.js leoloso/next-wordpress-starter funciona tanto com WPGraphQL quanto com Gato GraphQL. O starter usa a mesma lógica JS para ambos os servidores; apenas as queries GraphQL são diferentes.

Este starter fornece vários exemplos de adaptação de queries entre os dois servidores. Por exemplo, esta query WPGraphQL:

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

...é adaptada desta forma para o 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
}

Em detalhes: Executando o Gato GraphQL como uma aplicação PHP standalone

Aqui está a explicação detalhada do vídeo de demonstração apresentado anteriormente.

Fornecemos a query GraphQL a ser executada no arquivo retrieve-github-artifacts.gql.

A query conecta-se à API do GitHub obtendo o token de acesso da variável de ambiente GITHUB_ACCESS_TOKEN. Ela gera dinamicamente o caminho completo para o endpoint actions/artifacts a partir das variáveis fornecidas, e então envia uma requisição HTTP contra ele.

A partir da resposta, extrai a "URL de download" de dentro de cada item de artefato, e envia requisições HTTP assíncronas contra elas. Do header Location de cada uma dessas "URLs de download", obtemos a URL real do arquivo para download.

Por fim, imprime todas as URLs juntas separadas por um espaço, para facilitar a injeção no 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: " "
  )
}

A lógica PHP carrega diretamente o código do plugin Gato GraphQL e do bundle "Power Extensions" (necessário para enviar requisições HTTP e outras funcionalidades).

Como aplicação PHP standalone, devemos indicar explicitamente quais módulos são inicializados e fornecer qualquer configuração não padrão.

Por exemplo, dizemos ao módulo SendHTTPRequests para permitir a conexão com https://api.github.com/repos, e ao módulo EnvironmentFields para permitir o acesso à variável de ambiente GITHUB_ACCESS_TOKEN.

Note que o schema GraphQL é gerado na primeira vez que a query GraphQL é executada, e armazenado em cache no disco. Dessa forma, a partir da 2ª vez em diante, nenhum código para calcular o schema é executado, tornando a execução mais rápida.

Por fim, a aplicação standalone inicializa o servidor GraphQL, executa a query contra ele e imprime a resposta.

<?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();

Para executar a query GraphQL, rodamos no terminal (usando jq para formatar de forma legível a saída JSON):

php retrieve-github-artifacts.php | jq

Por fim, para extrair as URLs dos artefatos da resposta GraphQL e injetá-las no WP-CLI, executamos:

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

Como demonstrado no vídeo, somos capazes de executar o Gato GraphQL sem o WordPress.


Assine nossa newsletter

Fique por dentro de todas as atualizações do Gato GraphQL.