Blog

🤔 Por que o novo Gato GraphQL levou 1,5 anos para ser lançado?

Leonardo Losoviz
Por Leonardo Losoviz ·

A versão 0.9 do Gato GraphQL acaba de ser lançada. Foram necessários quase 1,5 anos de desenvolvimento e mais de 16.000 commits para ficar pronta. É muito tempo mesmo!

Ao compartilhar o anúncio no Hacker News, recebi a seguinte pergunta:

[...] Tenho curiosidade de saber o que exigiu 16k commits. Os projetos em que participei com mais de dez mil commits tinham muitas dezenas ou centenas de pessoas trabalhando em tempo integral. [...] Existe alguma complexidade que precisava ser superada e que o post não aborda?

A contagem de commits não é uma métrica muito confiável, pois eu posso fazer uma alteração muito simples e publicá-la como um único commit. Muitos desses 16k commits eram commits de "typo", ou apenas melhoravam uma descrição em algum README.

Mesmo assim, a contagem de commits dá uma ideia do esforço real envolvido. Havia também muitos commits repletos de modificações, incluindo dezenas, e até centenas de alterações de uma vez. As mudanças entre as versões 0.8 e 0.9 são de fato enormes, e isso exigiu esforço e tempo para ser realizado.

Neste post do blog, descreverei quais são essas mudanças, para explicar por que demorou tanto. E ao fazer isso, também darei uma prévia de alguns recursos avançados que foram adicionados à base de código, e que verão a luz do dia com a próxima versão 1.0.

Contexto do servidor GraphQL

Primeiro, vou compartilhar um pouco da história do motor e detalhes técnicos de como ele funciona.

(Isso é principalmente relevante para desenvolvedores; se você não está interessado em aspectos técnicos, fique à vontade para pular para a próxima seção.)

O Gato GraphQL é baseado no PoP, um motor que renderiza componentes em PHP (semelhante ao React ou Vue em JavaScript). Sua dependência desse motor é absoluta, e é por isso que o plugin está hospedado no monorepo GatoGraphQL/GatoGraphQL no GitHub.

Por baixo dos panos, essa dependência funciona assim:

O Gato GraphQL resolve uma query GraphQL transformando-a primeiro em um modelo de componentes equivalente, que o PoP resolve buscando todos os dados necessários, e então esses dados recebem o formato da query GraphQL.

Quando comecei a trabalhar no PoP por volta de 2013/2014, não havia GraphQL, e a metodologia para resolver um modelo de componentes em dados foi projetada e implementada do zero. A falta de um modelo para seguir (como o GraphQL para conceitos, e o projeto de referência graphql-js para uma implementação) foi tanto um obstáculo quanto uma bênção, como explicarei mais adiante.

O PoP foi inicialmente projetado para renderizar o site inteiro como HTML no lado do servidor, ao mesmo tempo em que expunha os dados brutos no formato JSON ao adicionar ?output=json à URL da página, e selecionando ainda quais dados recuperar (configurações, dados de objetos do banco de dados) com parâmetros de URL adicionais.

Clique nos links a seguir (todos apontando para a mesma página, apenas com parâmetros de URL diferentes) e observe como eles diferem:

Ao clicar no último link, uma percepção surge: isso é praticamente GraphQL! A única grande diferença é que os dados na resposta são implícitos, pois já foram definidos pelos componentes (em PHP) incluídos na página. O GraphQL, por outro lado, nos permite decidir quais dados buscar por meio de uma query.

Então, quando aprendi sobre GraphQL por volta de 2019, foi óbvio para mim fazer o PoP funcionar também como servidor GraphQL. Tudo o que ele precisava fazer era aceitar a query GraphQL como entrada e criar um modelo de componentes dinamicamente com base na query.

E foi isso que fiz. E funcionou bem. Mas era lento, porque o PoP entendia seu próprio formato de entrada, então a query GraphQL precisava ser adaptada ao formato PoP:

  1. Analisar a query GraphQL; depois
  2. Transformar a query no formato PoP; depois
  3. Analisar o formato PoP

A análise da query GraphQL era então feita duas vezes (uma vez para GraphQL, uma vez para PoP), e o formato PoP não estava sendo resolvido por meio de uma AST, mas apenas analisando a string da query repetidamente. (Não usar uma AST era uma codificação terrível, mas eu não tinha uma especificação para seguir, e seu desenvolvimento aconteceu organicamente, onde um simples substr(...) salvava o dia, todo dia.)

É por isso que digo que não ter a especificação GraphQL foi um obstáculo, pois minha solução era lenta (e essa era a situação na versão 0.8). Então decidi corrigir isso.

Convertendo o motor para GraphQL-first

A solução que escolhi foi fazer o PoP falar nativamente a linguagem GraphQL. Assim, passar uma query GraphQL ao PoP como entrada já seria convertido para o modelo de componentes, sem a necessidade de nenhum adaptador adicional, nem de fazer as coisas duas vezes.

Isso significava que o projeto PoP precisava ser reaproveitado, passando de uma biblioteca PHP que renderiza componentes para sites no lado do servidor adaptada para resolver queries GraphQL, para se tornar efetivamente um servidor GraphQL.

A base de código passou então por uma transformação massiva, introduzindo a AST GraphQL como fundação para comunicar o estado entre todos os serviços PHP no motor. Os objetos AST GraphQL são agora as entradas do PoP (em vez de strings de query).

Outros servidores GraphQL em PHP dependem de graphql-php, mas o plugin Gato GraphQL não. Isso é uma má notícia em relação ao esforço de manutenção (pois não posso reutilizar o que outra pessoa codificou), mas uma boa notícia em relação à independência: posso decidir adicionar recursos personalizados ao meu plugin no meu próprio ritmo e sob meu próprio critério (é por isso que o plugin já fornece o input object "oneof").

E como será mostrado na seção abaixo, isso é uma grande vantagem.

Incorporando recursos originais ao GraphQL

O GraphQL é normalmente associado à busca de dados. Naturalmente, você pode recuperar qualquer dado (posts, usuários, comentários, etc.) do Gato GraphQL:

query {
  posts(
    pagination: { limit: 5, offset: 20 }
    sort: { by: DATE, order: ASC }
  ) {
    id
    title
    content
    url
    author {
      id
      name
      url
    }
    comments {
      id
      date
      content
    }
  }
}

Mas isso é o básico. O GraphQL também pode ser usado para muitos outros casos de uso, incluindo manipulação e transformação de dados, e até mesmo colocar o GraphQL em um pipeline para mediar entre serviços.

Alguns exemplos onde o GraphQL é útil são:

  • Extrair informações de uma ou mais fontes (como usuários dos sites WordPress e os dados de contato da newsletter do Mailchimp), combinar os dados e analisá-los todos juntos como um único conjunto de dados
  • Executar operações para adaptar o conteúdo do site:
    • Uma única vez, como ao migrar um site para outro domínio e substituir "www.myoldsite.com" por "mynewsite.com" em todo o conteúdo e metadados
    • De forma contínua, como substituir qualquer "http://" por "https://" sempre que um escritor publica um novo post no blog
  • Conectar-se à API do Google Translate para traduzir todos os posts do blog para outro idioma
  • Enviar um tweet automaticamente após a publicação de um post no blog

O PoP havia sido projetado para suportar esses outros casos de uso, por meio de recursos que não são (naturalmente) suportados pelo GraphQL, como:

  • Suportar campos de "funcionalidade" (além dos campos de "dados"), que são adicionados a todos os tipos no schema
  • Passar o resultado de um campo como entrada para outro campo, dentro da mesma query
  • Compor directives, para que uma directive modifique o comportamento de outra directive
  • Decidir se aplica ou não uma directive dinamicamente, com base no valor do campo

E certamente não queria remover esses recursos do servidor GraphQL: eu já os havia codificado, e eles são certamente valiosos.

Portanto, a segunda razão pela qual a v0.9 demorou tanto é que eu também precisei encontrar uma maneira de incorporar essas novas capacidades ao GraphQL, de uma forma que não violasse a especificação GraphQL (por exemplo, introduzir novos elementos na sintaxe GraphQL estava fora de cogitação).

Um exemplo de manipulação de dados em GraphQL

As novas capacidades introduzidas ao GraphQL no plugin se tornarão mais visíveis no futuro próximo, quando a versão 1.0 for lançada. Mas você já pode ter um gostinho de algumas delas.

A seguinte query GraphQL recupera uma lista de entradas de usuários de uma API REST externa (que pode ser removida da resposta com @remove); insere esses dados em outro campo, dentro da mesma query; extrai a propriedade de email de cada entrada; e finalmente transforma o email em maiúsculas, mas apenas se o idioma nessa mesma entrada for inglês ou alemão:

###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes
{
  # Retrieve data from a REST API endpoint
  userEntries: _sendJSONObjectCollectionHTTPRequest(
    input: {
      url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
    }
  ) # @remove   # <= Uncomment this directive to not print the API data
 
  emails: _echo(value: $__userEntries)
 
    # Iterate all the entries, passing every entry
    # (under the dynamic variable $userEntry)
    # to each of the next 4 directives
    @underEachArrayItem(
      passValueOnwardsAs: "userEntry"
      affectDirectivesUnderPos: [1, 2, 3, 4]
    )
 
      # Extract property "lang" from the entry
      # via the functionality field `_objectProperty`,
      # and pass it onwards as dynamic variable $userLang
      @applyField(
        name: "_objectProperty"
        arguments: {
          object: $userEntry,
          by: {
            key: "lang"
          }
        }
        passOnwardsAs: "userLang"
      )
 
      # Execute functionality field `_inArray` to find out
      # if $userLang is either "en" or "de", and place the
      # result under dynamic variable $isSpecialLang
      @applyField(
        name: "_inArray"
        arguments: {
          value: $userLang,
          array: ["en", "de"]
        }
        passOnwardsAs: "isSpecialLang"
      )
 
      # Extract property "email" from the entry
      # and set it back as the value for that entry
      @applyField(
        name: "_objectProperty"
        arguments: {
          object: $userEntry,
          by: {
            key: "email"
          }
        }
        setResultInResponse: true
      )
 
      # If $isSpecialLang is `true` then execute
      # directive `@strUpperCase` 
      @if(condition: $isSpecialLang)
        @strUpperCase
}

Esta é a resposta (observe como apenas certos emails foram transformados em maiúsculas):

{
  "data": {
    "userEntries": [
      {
        "email": "abracadabra@ganga.com",
        "lang": "de"
      },
      {
        "email": "longon@caramanon.com",
        "lang": "es"
      },
      {
        "email": "rancotanto@parabara.com",
        "lang": "en"
      },
      {
        "email": "quezarapadon@quebrulacha.net",
        "lang": "fr"
      },
      {
        "email": "test@test.com",
        "lang": "de"
      },
      {
        "email": "emilanga@pedrola.com",
        "lang": "fr"
      }
    ],
    "emails": [
      "ABRACADABRA@GANGA.COM",
      "longon@caramanon.com",
      "RANCOTANTO@PARABARA.COM",
      "quezarapadon@quebrulacha.net",
      "TEST@TEST.COM",
      "emilanga@pedrola.com"
    ]
  }
}

Veja você mesmo! Pressione o botão "Run" para executar a query:

###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes {
  # Retrieve data from a REST API endpoint
  userEntries: _sendJSONObjectCollectionHTTPRequest(
    input: {
      url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
    }
  )
  # @remove   # <= Uncomment this directive to not print the API data
  emails: _echo(value: $__userEntries)
    # Iterate all the entries, passing every entry
    # (under the dynamic variable $userEntry)
    # to each of the next 4 directives
    @underEachArrayItem(
      passValueOnwardsAs: "userEntry"
      affectDirectivesUnderPos: [1, 2, 3, 4]
    )
      # Extract property "lang" from the entry
      # via the functionality field `_objectProperty`,
      # and pass it onwards as dynamic variable $userLang
      @applyField(
        name: "_objectProperty"
        arguments: { object: $userEntry, by: { key: "lang" } }
        passOnwardsAs: "userLang"
      )
      # Execute functionality field `_inArray` to find out
      # if $userLang is either "en" or "de", and place the
      # result under dynamic variable $isSpecialLang
      @applyField(
        name: "_inArray"
        arguments: { value: $userLang, array: ["en", "de"] }
        passOnwardsAs: "isSpecialLang"
      )
      # Extract property "email" from the entry
      # and set it back as the value for that entry
      @applyField(
        name: "_objectProperty"
        arguments: { object: $userEntry, by: { key: "email" } }
        setResultInResponse: true
      )
      # If $isSpecialLang is `true` then execute
      # directive `@strUpperCase`
      @if(condition: $isSpecialLang)
        @strUpperCase
}

Eu havia mencionado que não ser guiado pelo GraphQL foi um obstáculo, mas (em retrospecto) também uma bênção. Isso porque eu não tinha as restrições da especificação GraphQL, então pude me dar ao luxo de sonhar com essas novas capacidades.

E agora que esses recursos foram migrados para o Gato GraphQL, ele pode ser um aliado incrivelmente útil para tudo relacionado à recuperação, manipulação e transformação de conteúdo para o seu site WordPress. (Mesmo que eles estejam acessíveis apenas com a próxima v1.0).

Demorou um pouco, mas o esforço certamente valeu a pena.

Experimente!

Está convencido de que a longa espera valeu a pena? Espero que sim!

Vá em frente, baixe o plugin e confira:

Tem interesse em receber novidades sobre seu desenvolvimento, nova documentação e próximos lançamentos, incluindo a v1.0? Então fique à vontade para assinar a newsletter.

Quer explorar o código open source no GitHub? Confira GatoGraphQL/GatoGraphQL (e fique à vontade para dar uma estrela... Adoramos estrelas! ⭐️⭐️⭐️)

A propósito, quais transformações de conteúdo você precisa fazer no WordPress (para as quais talvez já esteja usando algum plugin comercial dedicado)? Por favor, me envie uma mensagem contando seu caso de uso.

Se você gostou do que viu, compartilhe com seus amigos e colegas, ajude a espalhar o amor ❤️.


Assine nossa newsletter

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