Blog

🤔 Perché il nuovo Gato GraphQL ha impiegato 1,5 anni per essere rilasciato?

Leonardo Losoviz
Di Leonardo Losoviz ·

La versione 0.9 di Gato GraphQL è stata appena rilasciata. Ci sono voluti quasi 1,5 anni di sviluppo e oltre 16000 commit per essere pronta. Davvero tanto tempo!

Condividendo l'annuncio su Hacker News, mi è stata posta la seguente domanda:

[...] Sono curioso di sapere cosa abbia richiesto 16k commit. I progetti su cui ho lavorato con più di diecimila commit avevano molte decine o centinaia di persone che lavoravano a tempo pieno. [...] C'è qualche complessità da superare che il post non affronta?

Il numero di commit non è una metrica molto affidabile, perché potrei fare un cambiamento molto semplice e pubblicarlo come un singolo commit. Molti di quei 16k commit erano commit di "typo", oppure miglioravano semplicemente una descrizione in qualche README.

Ciononostante, il numero di commit dà un'idea dell'effettivo sforzo richiesto. C'erano anche numerosi commit pieni di modifiche, comprese decine, e persino centinaia di cambiamenti alla volta. I cambiamenti tra le versioni 0.8 e 0.9 sono davvero enormi, e ciò ha richiesto sforzo e tempo per essere realizzato.

In questo articolo del blog descriverò quali sono questi cambiamenti, per spiegare perché ci è voluto così tanto tempo. E nel farlo, fornirò anche un'anteprima di alcune funzionalità avanzate aggiunte alla base di codice, che vedranno la luce con la prossima versione 1.0.

Contesto del server GraphQL

Per prima cosa, condividerò un po' della storia del motore e dei dettagli tecnici su come funziona.

(Questo è soprattutto rilevante per gli sviluppatori; se non sei interessato agli aspetti tecnici, sentiti libero di passare alla sezione successiva.)

Gato GraphQL è basato su PoP, un motore che renderizza componenti in PHP (simile a React o Vue in JavaScript). La sua dipendenza da questo motore è assoluta, ed è per questo che il plugin è ospitato nel monorepo GatoGraphQL/GatoGraphQL su GitHub.

Dietro le quinte, questa dipendenza ha questo aspetto:

Gato GraphQL risolve una query GraphQL trasformandola prima in un modello di componenti equivalente, che PoP risolve poi recuperando tutti i dati richiesti, dopodiché questi dati assumono la forma della query GraphQL.

Quando ho iniziato a lavorare su PoP intorno al 2013/2014, GraphQL non esisteva, e la metodologia per risolvere un modello di componenti in dati è stata progettata e implementata da zero. La mancanza di un modello da seguire (come GraphQL per i concetti, e il progetto di riferimento graphql-js per un'implementazione) è stata sia un ostacolo che una benedizione, come spiegherò più avanti.

PoP è stato inizialmente progettato per renderizzare l'intero sito web come HTML lato server, esponendo al contempo i dati grezzi in formato JSON aggiungendo ?output=json all'URL della pagina, e selezionando ulteriormente quali dati recuperare (impostazioni, dati degli oggetti DB) con parametri URL aggiuntivi.

Ti invito a cliccare sui seguenti link (tutti puntano alla stessa pagina web, solo con parametri URL diversi) e a notare come differiscono:

Cliccando sull'ultimo link, arriva un'illuminazione: questo è praticamente GraphQL! L'unica grande differenza è che i dati nella risposta sono impliciti, poiché sono già stati definiti dai componenti (in PHP) inclusi nella pagina. GraphQL, invece, ci permette di decidere quali dati recuperare tramite una query.

Così, quando ho conosciuto GraphQL intorno al 2019, è stato ovvio per me far sì che PoP funzionasse anche come server GraphQL. Tutto ciò che doveva fare era accettare la query GraphQL come input e creare al volo un modello di componenti basato sulla query.

Ed è quello che ho fatto. E ha funzionato bene. Ma era lento, perché PoP comprendeva il proprio formato di input, quindi la query GraphQL doveva essere adattata al formato PoP:

  1. Analizzare la query GraphQL; poi
  2. Trasformare la query nel formato PoP; poi
  3. Analizzare il formato PoP

L'analisi della query GraphQL veniva quindi effettuata due volte (una volta per GraphQL, una volta per PoP), e il formato PoP non veniva risolto tramite un AST, ma semplicemente analizzando di continuo la stringa della query. (Non usare un AST era una pessima pratica di codifica, ma non avevo una specifica da seguire, e il suo sviluppo è avvenuto in modo organico, dove un semplice substr(...) salvava la situazione, ogni giorno.)

Ecco perché dico che non avere la specifica GraphQL è stato un ostacolo, poiché la mia soluzione era lenta (e questa era la situazione con la versione 0.8). Così ho deciso di correggerla.

Convertire il motore in GraphQL-first

La soluzione che ho scelto è far sì che PoP parli nativamente il linguaggio GraphQL. In questo modo, passare una query GraphQL a PoP come input sarebbe già convertito in modello di componenti, senza bisogno di alcun adattatore aggiuntivo, né di fare le cose due volte.

Ciò significava che il progetto PoP doveva essere riconvertito, passando da una libreria PHP che renderizza componenti per siti web lato server, adattata a risolvere query GraphQL, a diventare effettivamente un server GraphQL.

La base di codice ha quindi subito una trasformazione massiccia, introducendo l'AST GraphQL come fondamento per comunicare lo stato tra tutti i servizi PHP del motore. Gli oggetti AST GraphQL sono ora gli input di PoP (invece delle stringhe di query).

Altri server GraphQL in PHP si affidano a graphql-php, ma il plugin Gato GraphQL no. È una cattiva notizia per quanto riguarda lo sforzo di manutenzione (perché non posso riutilizzare ciò che qualcun altro ha codificato), ma una buona notizia per quanto riguarda l'indipendenza: posso decidere di aggiungere funzionalità personalizzate al mio plugin alla mia velocità e secondo i miei criteri (ecco perché il plugin fornisce già l'input object "oneof").

E come mostrerà la sezione qui sotto, questo è un grande vantaggio.

Incorporare funzionalità originali in GraphQL

GraphQL è normalmente associato al recupero dei dati. Naturalmente, puoi recuperare qualsiasi dato (articoli, utenti, commenti, ecc.) da 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
    }
  }
}

Ma questa è la parte facile. GraphQL può anche essere usato per molti altri casi d'uso, tra cui la manipolazione e la trasformazione dei dati, e persino l'inserimento di GraphQL in una pipeline per fare da intermediario tra servizi.

Alcuni esempi in cui GraphQL è utile sono:

  • Estrarre informazioni da una o più fonti (come gli utenti dei siti WordPress e i dati di contatto della newsletter da Mailchimp), combinare i dati e analizzarli tutti insieme come un unico insieme di dati
  • Eseguire operazioni per adattare il contenuto del sito:
    • Una tantum, come quando si migra un sito verso un altro dominio sostituendo "www.myoldsite.com" con "mynewsite.com" ovunque nel contenuto e nei metadati
    • In modo continuativo, come sostituire ogni "http://" con "https://" ogni volta che un redattore pubblica un nuovo articolo del blog
  • Connettersi all'API di Google Translate per tradurre tutti gli articoli del blog in un'altra lingua
  • Inviare automaticamente un tweet dopo la pubblicazione di un articolo del blog

PoP era stato progettato per supportare questi altri casi d'uso, tramite funzionalità che non sono (naturalmente) supportate da GraphQL, come:

  • Supportare campi di "funzionalità" (oltre ai campi di "dati"), che vengono aggiunti a tutti i tipi nello schema
  • Passare il risultato di un campo come input a un altro campo, all'interno della stessa query
  • Comporre direttive, in modo che una direttiva modifichi il comportamento di un'altra direttiva
  • Decidere se applicare o meno una direttiva in modo dinamico, in base al valore del campo

E di certo non volevo rimuovere queste funzionalità dal server GraphQL: le avevo già codificate, e sono certamente preziose.

Quindi la seconda ragione per cui v0.9 ha richiesto così tanto tempo è che ho anche dovuto trovare un modo per incorporare queste nuove capacità in GraphQL, in un modo che non violasse la specifica GraphQL (per esempio, introdurre nuovi elementi nella sintassi GraphQL era da escludere).

Un esempio di manipolazione dei dati in GraphQL

Le nuove capacità introdotte in GraphQL dal plugin diventeranno più visibili nel prossimo futuro, con il rilascio della versione 1.0. Ma puoi già averne un assaggio.

La seguente query GraphQL recupera un elenco di voci di utenti da un'API REST esterna (che può essere rimossa dalla risposta con @remove); inserisce questi dati in un altro campo, all'interno della stessa query; estrae la proprietà email da ogni voce; e infine trasforma l'email in maiuscolo, ma solo se la lingua di quella stessa voce è l'inglese o il tedesco:

###################################################################
# 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
}

Ecco la risposta (nota come solo alcune email siano state trasformate in maiuscolo):

{
  "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"
    ]
  }
}

Verificalo tu stesso! Premi il pulsante "Run" per eseguire la 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
}

Avevo accennato che non essere guidato da GraphQL è stato un ostacolo, ma (col senno di poi) anche una benedizione. Questo perché non avevo i vincoli della specifica GraphQL, quindi potevo permettermi di sognare queste nuove capacità.

E ora che queste funzionalità sono state migrate in Gato GraphQL, esso può essere un alleato incredibilmente utile per tutto ciò che riguarda il recupero, la manipolazione e la trasformazione dei contenuti per il tuo sito WordPress. (Anche se saranno accessibili solo con la prossima v1.0).

Ci è voluto un po', ma lo sforzo ne è valso sicuramente la pena.

Provalo!

Sei convinto che la lunga attesa ne sia valsa la pena? Lo spero!

Vai avanti, scarica il plugin e provalo:

Vuoi ricevere notizie sul suo sviluppo, sulla nuova documentazione e sulle prossime versioni, inclusa la v1.0? Allora sei il benvenuto a iscriverti alla newsletter.

Vuoi esplorare il codice open source su GitHub? Dai un'occhiata a GatoGraphQL/GatoGraphQL (e sentiti libero di lasciargli una stella... Adoriamo le stelle! ⭐️⭐️⭐️)

A proposito, quali trasformazioni di contenuti devi effettuare in WordPress (per le quali magari stai già usando qualche plugin commerciale dedicato)? Per favore, inviami un messaggio raccontandomi il tuo caso d'uso.

Se ti piace ciò che vedi, condividilo con i tuoi amici e colleghi, aiuta a diffondere l'amore ❤️.


Iscriviti alla nostra newsletter

Resta aggiornato su tutte le novità di Gato GraphQL.