🤔 Waarom duurde het 1,5 jaar om de nieuwe Gato GraphQL uit te brengen?
Versie 0.9 van Gato GraphQL is zojuist uitgebracht. Het kostte bijna 1,5 jaar ontwikkeling en meer dan 16.000 commits om klaar te zijn. Dat is inderdaad lang!
Na het delen van de aankondiging op Hacker News, ontving ik de volgende vraag:
[...] Ik ben benieuwd wat 16k commits kostte. De projecten waaraan ik heb meegewerkt met meer dan tienduizend commits hadden wel tientallen of honderden mensen die fulltime werkten. [...] Is er een complexiteit die overwonnen moest worden waar de post niet op ingaat?
Het aantal commits is geen erg betrouwbare maatstaf, want ik kan een heel eenvoudige wijziging doorvoeren en die als één commit pushen. Veel van die 16k commits waren "typo"-commits, of gewoon een verbeterde beschrijving in een README.
Desalniettemin geeft het aantal commits een idee van de daadwerkelijke inspanning. Er waren ook veel commits vol met wijzigingen, inclusief tientallen, en soms zelfs honderden aanpassingen tegelijk. De wijzigingen tussen versies 0.8 en 0.9 zijn inderdaad enorm, en dat kostte moeite en tijd om voor elkaar te krijgen.
In dit blogbericht beschrijf ik wat die wijzigingen zijn, om te verklaren waarom het zo lang duurde. En daarbij geef ik ook alvast een voorproefje van enkele geavanceerde functies die aan de codebase zijn toegevoegd en die het daglicht zullen zien met de aankomende versie 1.0.
Achtergrond van de GraphQL-server
Eerst deel ik een beetje de geschiedenis van de engine en technische details over hoe die werkt.
(Dit is voornamelijk relevant voor ontwikkelaars; als je niet geïnteresseerd bent in technische zaken, ben je welkom om door te gaan naar de volgende sectie.)
Gato GraphQL is gebouwd op PoP, een engine die componenten rendert in PHP (vergelijkbaar met React of Vue in JavaScript). De afhankelijkheid van deze engine is absoluut, daarom wordt de plugin gehost in de GatoGraphQL/GatoGraphQL-monorepo op GitHub.
Onder de motorkap ziet deze afhankelijkheid er als volgt uit:
Gato GraphQL lost een GraphQL-query op door deze eerst om te zetten naar een equivalent componentmodel, dat PoP vervolgens oplost door alle benodigde gegevens op te halen, en daarna krijgen deze gegevens de vorm van de GraphQL-query.
Toen ik ergens rond 2013/2014 begon te werken aan PoP, bestond GraphQL nog niet, en de methode voor het omzetten van een componentmodel naar gegevens werd van nul af aan ontworpen en geïmplementeerd. Het ontbreken van een model om te volgen (zoals GraphQL voor concepten, en het graphql-js-referentieproject voor een implementatie) was zowel een belemmering als een zegen, zoals ik later zal uitleggen.
PoP was aanvankelijk ontworpen om de hele website als HTML aan de serverkant te renderen, terwijl de ruwe gegevens in JSON-formaat beschikbaar werden gesteld door ?output=json aan de URL van de pagina toe te voegen, en verder te selecteren welke gegevens opgehaald moesten worden (instellingen, DB-objectgegevens) met aanvullende URL-parameters.
Klik gerust op de volgende links (die allemaal naar dezelfde webpagina wijzen, maar met andere URL-parameters) en merk op hoe ze van elkaar verschillen:
- HTML-inhoud: mesym.com/en/posts/
- Ruwe JSON-gegevens (instellingen + DB): mesym.com/en/posts/?output=json
- Ruwe JSON-gegevens (DB): mesym.com/en/posts/?output=json&module=data
Wanneer je op de laatste link klikt, komt er een inzicht: dit is vrijwel GraphQL! Het enige grote verschil is dat de gegevens in de response impliciet zijn, omdat ze al zijn gedefinieerd door de componenten (in PHP) die op de pagina zijn opgenomen. GraphQL daarentegen stelt ons in staat te bepalen welke gegevens we ophalen via een query.
Toen ik dus ergens rond 2019 meer over GraphQL leerde, was het voor mij een logische stap om PoP ook een GraphQL-server te laten bedienen. Het enige wat nodig was, was de GraphQL-query als invoer accepteren en on-the-fly een componentmodel op basis van de query aanmaken.
En dat heb ik gedaan. En het werkte goed. Maar het was langzaam, omdat PoP zijn eigen invoerformaat begreep, zodat de GraphQL-query moest worden aangepast aan het PoP-formaat:
- De GraphQL-query parsen; daarna
- De query omzetten naar het PoP-formaat; daarna
- Het PoP-formaat parsen
Het parsen van de GraphQL-query werd dan twee keer gedaan (één keer voor GraphQL, één keer voor PoP), en het PoP-formaat werd niet verwerkt via een AST, maar gewoon door de query-string steeds opnieuw te parsen. (Geen AST gebruiken was slechte code, maar ik had geen specificatie om te volgen, en de ontwikkeling ervan verliep organisch, waarbij een eenvoudige substr(...) elke dag de dag redde.)
Daarom zeg ik dat het ontbreken van de GraphQL-specificatie een belemmering was, want mijn oplossing was traag (en dat was de situatie bij versie 0.8). Dus besloot ik het te verhelpen.
De engine omzetten naar GraphQL-first
De oplossing die ik heb gekozen is om PoP van nature de GraphQL-taal te laten spreken. Dan zou het doorgeven van een GraphQL-query aan PoP als invoer al worden omgezet naar het componentmodel, zonder dat er een aanvullende adapter nodig is of dingen twee keer gedaan moeten worden.
Dit betekende dat het PoP-project opnieuw moest worden ingericht: van een PHP-bibliotheek die componenten voor websites aan de serverkant rendert en was aangepast om GraphQL-queries op te lossen, naar daadwerkelijk een GraphQL-server worden.
De codebase onderging vervolgens een enorme transformatie, waarbij de GraphQL AST als basis werd geïntroduceerd om de toestand tussen alle PHP-services in de engine te communiceren. GraphQL AST-objecten zijn nu de invoer voor PoP (in plaats van query-strings).
Andere GraphQL-servers in PHP vertrouwen op graphql-php, maar de plugin Gato GraphQL niet. Dat is slecht nieuws wat betreft de onderhoudsinspanning (want ik kan de code van iemand anders niet hergebruiken), maar goed nieuws wat betreft onafhankelijkheid: ik kan zelf beslissen wanneer en hoe ik aangepaste functies aan mijn plugin toevoeg (daarom biedt de plugin al het input object "oneof").
En zoals in de sectie hieronder zal worden aangetoond, is dit een groot voordeel.
Originele functies toevoegen aan GraphQL
GraphQL wordt normaal gesproken geassocieerd met het ophalen van gegevens. Vanzelfsprekend kun je alle gewenste gegevens (berichten, gebruikers, reacties, enz.) ophalen uit 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
}
}
}Maar dit is laaghangende vruchten plukken. GraphQL kan ook voor veel andere toepassingen worden gebruikt, waaronder gegevensmanipulatie en -transformatie, en zelfs het inzetten van GraphQL in een pipeline als bemiddelaar tussen services.
Enkele voorbeelden waarbij GraphQL nuttig is:
- Informatie extraheren uit een of meer bronnen (zoals gebruikers van de WordPress-sites en de nieuwsbriefcontactgegevens uit Mailchimp), de gegevens combineren en ze als één dataset analyseren
- Bewerkingen uitvoeren om de inhoud op de site aan te passen:
- Eenmalig, zoals bij het migreren van een site naar een ander domein en overal in de inhoud en metadata
"www.myoldsite.com"vervangen door"mynewsite.com" - Doorlopend, zoals elk
"http://"vervangen door"https://"telkens wanneer een schrijver een nieuw blogbericht publiceert
- Eenmalig, zoals bij het migreren van een site naar een ander domein en overal in de inhoud en metadata
- Verbinding maken met de Google Translate API om alle blogberichten naar een andere taal te vertalen
- Automatisch een tweet versturen nadat een blogbericht is gepubliceerd
PoP was ontworpen om deze andere gebruiksscenario's te ondersteunen, via functies die (van nature) niet worden ondersteund door GraphQL, zoals:
- Ondersteuning van "functionaliteits"-velden (naast "data"-velden), die aan alle typen in het schema worden toegevoegd
- Het resultaat van een veld doorgeven als invoer aan een ander veld, binnen dezelfde query
- Directives samenvoegen, zodat een directive het gedrag van een andere directive kan aanpassen
- Dynamisch beslissen om een directive al dan niet toe te passen, op basis van de waarde van het veld
En ik wilde deze functies zeker niet uit de GraphQL-server verwijderen: ik had ze al gecodeerd, en ze zijn zeker waardevol.
De tweede reden waarom v0.9 zo lang duurde, is dan ook dat ik ook een manier moest vinden om deze nieuwe mogelijkheden in GraphQL te integreren, op een manier die de GraphQL-specificatie niet verbrak (het introduceren van nieuwe elementen in de GraphQL-syntaxis was bijvoorbeeld niet toegestaan).
Een voorbeeld van gegevensmanipulatie in GraphQL
De nieuwe mogelijkheden die in de plugin aan GraphQL zijn toegevoegd, worden in de nabije toekomst duidelijker zichtbaar, wanneer versie 1.0 wordt uitgebracht. Maar je kunt nu al een voorproefje krijgen van een aantal ervan.
De volgende GraphQL-query haalt een lijst met gebruikersvermeldingen op uit een externe REST API (die @removed kan worden uit de response); voert deze gegevens in een ander veld in, binnen dezelfde query; extraheert de e-maileigenschap uit elke vermelding; en zet ten slotte het e-mailadres om naar hoofdletters, maar alleen als de taal in diezelfde vermelding Engels of Duits is:
###################################################################
# 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
}Dit is de response (let op hoe alleen bepaalde e-mailadressen in hoofdletters zijn gezet):
{
"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"
]
}
}Probeer het zelf! Druk op de knop "Run" om de query uit te voeren:
###################################################################
# 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
}Ik had al gezegd dat het ontbreken van de GraphQL-specificatie als leidraad een belemmering was, maar (achteraf gezien) ook een zegen. Dit is omdat ik niet de beperkingen van de GraphQL-specificatie had, zodat ik me kon veroorloven te dromen van deze nieuwe mogelijkheden.
En nu deze functies zijn overgedragen naar Gato GraphQL, kan het een ongelooflijk nuttige bondgenoot zijn voor alles wat te maken heeft met het ophalen, bewerken en transformeren van inhoud voor je WordPress-site. (Ook al zijn ze pas beschikbaar met de aankomende v1.0.)
Het duurde even, maar de inspanning was zeker de moeite waard.
Probeer het uit!
Ben je ervan overtuigd dat het lange wachten de moeite waard was? Ik hoop het!
Ga je gang, download de plugin en bekijk hem:
Wil je nieuws ontvangen over de ontwikkeling, nieuwe documentatie en aankomende releases, waaronder v1.0? Dan ben je van harte welkom om je aan te melden voor de nieuwsbrief.
Wil je de open-sourcecode op GitHub verkennen? Bekijk GatoGraphQL/GatoGraphQL (en je bent van harte welkom om een ster te geven... We houden van sterren! ⭐️⭐️⭐️)
Trouwens, welke inhoudsmanipulaties moet je doen in WordPress (waarvoor je misschien al een speciale commerciële plugin gebruikt)? Stuur me gerust een bericht met je gebruiksscenario.
Als je het leuk vindt wat je ziet, deel het dan met je vrienden en collega's en help de liefde te verspreiden ❤️.