đ€ Pourquoi le nouveau Gato GraphQL a-t-il mis 1,5 an Ă sortir ?
La version 0.9 de Gato GraphQL vient d'ĂȘtre publiĂ©e. Il a fallu prĂšs de 1,5 an de dĂ©veloppement et plus de 16 000 commits pour ĂȘtre prĂȘte. C'est vraiment beaucoup de temps !
En partageant l'annonce sur Hacker News, j'ai reçu la question suivante :
[...] Je suis curieux de savoir ce qui a nécessité 16k commits. Les projets sur lesquels j'ai travaillé avec plus de dix mille commits avaient de nombreuses dizaines ou centaines de personnes travaillant à temps plein. [...] Y a-t-il une complexité à surmonter que le billet ne mentionne pas ?
Le nombre de commits n'est pas une métrique trÚs fiable, car je peux faire un changement trÚs simple et le pousser comme un seul commit. Beaucoup de ces 16k commits étaient des commits de "typo", ou amélioraient simplement une description dans un README.
Néanmoins, le nombre de commits donne une idée de l'effort réel impliqué. Il y avait aussi de nombreux commits chargés de modifications, incluant des dizaines, voire des centaines de changements à la fois. Les changements entre les versions 0.8 et 0.9 sont en effet énormes, et cela a demandé des efforts et du temps.
Dans ce billet de blog, je décrirai quels sont ces changements, afin d'expliquer pourquoi cela a pris autant de temps. Ce faisant, je donnerai également un aperçu de certaines fonctionnalités avancées ajoutées à la base de code, qui verront le jour avec la prochaine version 1.0.
Contexte du serveur GraphQL
D'abord, je partagerai un peu de l'histoire du moteur et des détails techniques sur son fonctionnement.
(Ceci est surtout pertinent pour les dĂ©veloppeurs ; si vous n'ĂȘtes pas intĂ©ressĂ© par les aspects techniques, n'hĂ©sitez pas Ă passer Ă la section suivante.)
Gato GraphQL est basé sur PoP, un moteur qui rend des composants en PHP (similaire à React ou Vue en JavaScript). Sa dépendance à ce moteur est absolue, c'est pourquoi le plugin est hébergé sous le monorepo GatoGraphQL/GatoGraphQL sur GitHub.
Sous le capot, cette dépendance ressemble à ceci :
Gato GraphQL rĂ©sout une requĂȘte GraphQL en la transformant d'abord en un modĂšle de composants Ă©quivalent, que PoP rĂ©sout ensuite en rĂ©cupĂ©rant toutes les donnĂ©es requises, puis ces donnĂ©es prennent la forme de la requĂȘte GraphQL.
Quand j'ai commencé à travailler sur PoP aux alentours de 2013/2014, GraphQL n'existait pas, et la méthodologie pour résoudre un modÚle de composants en données a été conçue et implémentée de zéro. L'absence d'un modÚle à suivre (comme GraphQL pour les concepts, et le projet de référence graphql-js pour une implémentation) a été à la fois un obstacle et une bénédiction, comme je l'expliquerai plus loin.
PoP a été initialement conçu pour rendre l'ensemble du site web en HTML cÎté serveur, tout en exposant les données brutes au format JSON en ajoutant ?output=json à l'URL de la page, et en sélectionnant davantage quelles données récupérer (paramÚtres, données d'objets DB) avec des paramÚtres URL supplémentaires.
Veuillez cliquer sur les liens suivants (tous pointant vers la mĂȘme page web, mais avec diffĂ©rents paramĂštres URL) et remarquer comment ils diffĂšrent :
- Contenu HTML : mesym.com/en/posts/
- Données JSON brutes (paramÚtres + DB) : mesym.com/en/posts/?output=json
- Données JSON brutes (DB) : mesym.com/en/posts/?output=json&module=data
En cliquant sur le dernier lien, une Ă©vidence s'impose : c'est pratiquement GraphQL ! La seule grande diffĂ©rence est que les donnĂ©es dans la rĂ©ponse sont implicites, car elles ont dĂ©jĂ Ă©tĂ© dĂ©finies par les composants (en PHP) inclus dans la page. GraphQL, en revanche, nous permet de dĂ©cider quelles donnĂ©es rĂ©cupĂ©rer via une requĂȘte.
Ainsi, quand j'ai appris GraphQL aux alentours de 2019, il m'a semblĂ© Ă©vident de faire en sorte que PoP satisfasse Ă©galement un serveur GraphQL. Tout ce qu'il avait Ă faire Ă©tait d'accepter la requĂȘte GraphQL comme entrĂ©e et de crĂ©er un modĂšle de composants Ă la volĂ©e basĂ© sur la requĂȘte.
Et c'est ce que j'ai fait. Et ça fonctionnait bien. Mais c'Ă©tait lent, car PoP comprenait son propre format d'entrĂ©e, donc la requĂȘte GraphQL devait ĂȘtre adaptĂ©e au format PoP :
- Analyser la requĂȘte GraphQL ; puis
- Transformer la requĂȘte au format PoP ; puis
- Analyser le format PoP
L'analyse de la requĂȘte GraphQL Ă©tait donc effectuĂ©e deux fois (une fois pour GraphQL, une fois pour PoP), et le format PoP n'Ă©tait pas rĂ©solu via un AST, mais simplement en analysant la chaĂźne de requĂȘte encore et encore. (Ne pas utiliser un AST Ă©tait une mauvaise pratique de codage, mais je n'avais pas de spĂ©cification Ă suivre, et son dĂ©veloppement s'est fait de maniĂšre organique, oĂč un simple substr(...) sauvait la mise, chaque jour.)
C'est pourquoi je dis que ne pas avoir la spécification GraphQL était un obstacle, car ma solution était lente (et c'était la situation avec la version 0.8). J'ai donc décidé de corriger ça.
Convertir le moteur en GraphQL-first
La solution que j'ai choisie est de faire en sorte que PoP parle nativement le langage GraphQL. Ainsi, passer une requĂȘte GraphQL Ă PoP comme entrĂ©e serait dĂ©jĂ converti en modĂšle de composants, sans besoin d'adaptateur supplĂ©mentaire ni de faire les choses deux fois.
Cela signifiait que le projet PoP devait ĂȘtre rĂ©orientĂ©, passant d'une bibliothĂšque PHP qui rend des composants pour des sites web cĂŽtĂ© serveur adaptĂ©e Ă la rĂ©solution de requĂȘtes GraphQL, Ă devenir rĂ©ellement un serveur GraphQL.
La base de code a alors subi une transformation massive, introduisant l'AST GraphQL comme fondation pour communiquer l'Ă©tat Ă travers tous les services PHP du moteur. Les objets AST GraphQL sont dĂ©sormais les entrĂ©es de PoP (au lieu des chaĂźnes de requĂȘte).
D'autres serveurs GraphQL en PHP s'appuient sur graphql-php, mais le plugin Gato GraphQL ne le fait pas. C'est une mauvaise nouvelle concernant l'effort de maintenance (car je ne peux pas réutiliser ce que quelqu'un d'autre a codé), mais une bonne nouvelle concernant l'indépendance : je peux décider d'ajouter des fonctionnalités personnalisées à mon plugin à ma propre vitesse, selon mes propres critÚres (c'est pourquoi le plugin fournit déjà l'input object "oneof").
Et comme le montrera la section ci-dessous, c'est un grand avantage.
Incorporer des fonctionnalités originales à GraphQL
GraphQL est normalement associé à la récupération de données. Naturellement, vous pouvez récupérer n'importe quelle donnée (articles, utilisateurs, commentaires, etc.) depuis 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
}
}
}Mais c'est la partie facile. GraphQL peut Ă©galement ĂȘtre utilisĂ© pour de nombreux autres cas d'usage, notamment la manipulation et la transformation de donnĂ©es, et mĂȘme le placement de GraphQL dans un pipeline pour servir d'intermĂ©diaire entre des services.
Voici quelques exemples oĂč GraphQL est utile :
- Extraire des informations d'une ou plusieurs sources (comme les utilisateurs des sites WordPress et les données de contact de la newsletter depuis Mailchimp), combiner les données et les analyser ensemble comme un seul jeu de données
- Exécuter des opérations pour adapter le contenu du site :
- Une seule fois, comme lors de la migration d'un site vers un autre domaine en remplaçant
"www.myoldsite.com"par"mynewsite.com"partout dans le contenu et les métadonnées - De maniÚre continue, comme remplacer tout
"http://"par"https://"chaque fois qu'un rédacteur publie un nouveau billet de blog
- Une seule fois, comme lors de la migration d'un site vers un autre domaine en remplaçant
- Se connecter Ă l'API Google Translate pour traduire tous les billets de blog dans une autre langue
- Envoyer un tweet automatiquement aprĂšs la publication d'un billet de blog
PoP avait été conçu pour supporter ces autres cas d'usage, via des fonctionnalités qui ne sont pas (naturellement) supportées par GraphQL, telles que :
- Supporter des champs de "fonctionnalité" (en plus des champs de "données"), qui sont ajoutés à tous les types dans le schéma
- Passer le rĂ©sultat d'un champ comme entrĂ©e Ă un autre champ, dans la mĂȘme requĂȘte
- Composer des directives, pour qu'une directive modifie le comportement d'une autre directive
- Décider d'appliquer ou non une directive de maniÚre dynamique, en fonction de la valeur du champ
Et je ne voulais certainement pas supprimer ces fonctionnalités du serveur GraphQL : je les avais déjà codées, et elles sont certainement précieuses.
La deuxiÚme raison pour laquelle v0.9 a pris autant de temps est donc que j'ai également dû trouver un moyen d'incorporer ces nouvelles capacités à GraphQL, d'une maniÚre qui ne brise pas la spécification GraphQL (par exemple, introduire de nouveaux éléments à la syntaxe GraphQL était exclu).
Un exemple de manipulation de données dans GraphQL
Les nouvelles capacités introduites dans GraphQL par le plugin deviendront plus visibles dans un proche avenir, lors de la sortie de la version 1.0. Mais vous pouvez déjà en avoir un avant-goût.
La requĂȘte GraphQL suivante rĂ©cupĂšre une liste d'entrĂ©es d'utilisateurs depuis une API REST externe (qui peut ĂȘtre @remove-Ă©e de la rĂ©ponse) ; injecte ces donnĂ©es dans un autre champ, dans la mĂȘme requĂȘte ; extrait la propriĂ©tĂ© email de chaque entrĂ©e ; et transforme finalement l'email en majuscules, mais seulement si la langue de cette mĂȘme entrĂ©e est l'anglais ou l'allemand :
###################################################################
# 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
}Voici la réponse (remarquez comment seuls certains emails ont été mis en majuscules) :
{
"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"
]
}
}VĂ©rifiez-le vous-mĂȘme ! Appuyez sur le bouton "Run" pour exĂ©cuter la requĂȘte :
###################################################################
# 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
}J'avais mentionnĂ© que ne pas ĂȘtre guidĂ© par GraphQL Ă©tait un obstacle, mais (rĂ©trospectivement) aussi une bĂ©nĂ©diction. C'est parce que je n'avais pas les contraintes de la spĂ©cification GraphQL, je pouvais donc me permettre de rĂȘver Ă ces nouvelles capacitĂ©s.
Et maintenant que ces fonctionnalitĂ©s ont Ă©tĂ© migrĂ©es vers Gato GraphQL, il peut ĂȘtre un alliĂ© incroyablement utile pour tout ce qui concerne la rĂ©cupĂ©ration, la manipulation et la transformation de contenu pour votre site WordPress. (MĂȘme si elles ne seront accessibles qu'avec la prochaine v1.0).
Ăa a pris du temps, mais l'effort en valait vraiment la peine.
Essayez-le !
Ătes-vous convaincu que la longue attente en valait la peine ? Je l'espĂšre !
Allez-y, téléchargez le plugin et découvrez-le :
Vous souhaitez recevoir des nouvelles concernant son développement, une nouvelle documentation et les prochaines versions, y compris v1.0 ? N'hésitez pas à vous abonner à la newsletter.
Vous voulez explorer le code open source sur GitHub ? DĂ©couvrez GatoGraphQL/GatoGraphQL (et n'hĂ©sitez pas Ă lui donner une Ă©toile... Nous adorons les Ă©toiles ! âïžâïžâïž)
Au fait, quelles transformations de contenu devez-vous effectuer dans WordPress (pour lesquelles vous utilisez peut-ĂȘtre dĂ©jĂ un plugin commercial dĂ©diĂ©) ? Veuillez m'envoyer un message pour me parler de votre cas d'usage.
Si ce que vous voyez vous plaĂźt, partagez-le avec vos amis et collĂšgues, aidez Ă rĂ©pandre l'amour â€ïž.