🕸 Hoe en waar kan GraphQL WordPress verbeteren als aanvulling op de REST API
Update 01/05/2024: Bekijk de vergelijking Gato GraphQL vs WP REST API.
Afgelopen weekend publiceerde ik de blogpost 🦸🏿♂️ Gato GraphQL is nu getranspileerd van PHP 8.0 naar 7.1.
Na het delen van de post op Reddit's /r/php, startte de community een levendige discussie over hoe nuttig het is om GraphQL in WordPress te gebruiken, hoe het verschilt van de WP REST API, en of het gerechtvaardigd is om nog een API aan WordPress toe te voegen.
Ik denk dat de meeste opmerkingen raak zijn, en andere missen wat cruciale informatie. GraphQL is niet alleen een interface, maar ook een implementatie. Dit betekent dat verschillende GraphQL-servers, van verschillende aanbieders, mogelijk zijn ontworpen met verschillende prioriteiten in gedachten. Daarom kunnen we niet altijd een uniforme verwachting hebben van wat GraphQL biedt, of een volledig begrip van hoe een GraphQL-engine werkt.
Zo zal de GraphQL-ervaring in WordPress en in Laravel verschillend zijn, net als de ervaring die de verschillende servers bieden: WPGraphQL of Gato GraphQL.
Dit artikel is mijn kijk op de zaak, waarbij ik een aantal opmerkingen uit de Reddit-post behandel.
GraphQL vs WP REST API
[Zo'n slecht idee] om een GraphQL API bovenop WordPress te hebben, dat al zijn eigen REST API gebruikt. Gebruik gewoon de REST API. [Source]
Zowel de REST API als GraphQL dienen hetzelfde doel: de applicatie voorzien van de benodigde data. Ze gedragen zich echter anders in hoe ze dit bereiken: terwijl REST vooraf gedefinieerde endpoints heeft die een specifieke set data leveren, kan GraphQL precies de data leveren die nodig is.
Dit verschillende gedrag kan een directe impact hebben op de prestaties van de applicatie. Met REST, als we een lijst van posts plus wat data van elke auteur van de post moeten ophalen, zijn er extra verzoeken nodig. Mogelijk 1 extra verzoek voor alle auteursdata, of 1 extra verzoek per auteur. Ondertussen wacht de bezoeker van de website mogelijk op de weergave van de pagina.
GraphQL verbetert deze situatie, omdat we alle post- en auteursdata in één verzoek kunnen ophalen, en de weergave van de webpagina sneller zal zijn:
{
posts {
id
title
excerpt
date
url
author {
id
name
url
}
}
}Zelfs als we de REST API al hebben in WordPress, betekent dat niet dat het altijd het meest geschikte hulpmiddel is voor elke taak. Natuurlijk kunnen we het altijd gebruiken, maar als we ook toegang hebben tot GraphQL, kunnen we ervoor kiezen deze API te gebruiken wanneer het een voordeel biedt boven REST, en we zijn er beter mee af.
Moeilijke initiële setup voor GraphQL + Het moeten schrijven van resolvers
Er valt zeker iets voor te zeggen dat de initiële setup voor GraphQL exponentieel hoger is dan voor REST; je hebt gelijk dat de associaties geconfigureerd moeten worden. [Source]
En...
Wat jij en bijna iedereen op het web weglaat, is dat om dit API-formaat te laten werken, je de parser (resolvers + types) moet schrijven, wat een reeks problemen met zich meebrengt die niet aanwezig zijn bij REST. [Source]
Deze opmerkingen zijn niet helemaal accuraat, omdat zowel WPGraphQL als Gato GraphQL het WordPress-datamodel al in het GraphQL-schema hebben vastgelegd (WPGraphQL volledig, mijn plugin grotendeels).
Na het installeren van een van deze plugins kun je dus meteen data beginnen op te halen voor je applicatie, zonder de noodzaak om resolvers te maken of associaties tussen entiteiten in te stellen.
Het is waar dat, om aangepaste data op te halen van de eigen entiteiten van de applicatie (zoals CPT's), deze via resolvers in kaart gebracht moeten worden, en dat je dit zelf moet doen. Maar dit verschilt niet van REST: als je aangepaste data van je CPT nodig hebt, moet je een REST-endpoint maken om die data op te halen. Een aangepast endpoint is ook een resolver.
Wat de behoefte aan resolvers betreft, zijn REST en de GraphQL API dus vrijwel hetzelfde.
Nu, als je websites en documentatie doorleest, wekt het de indruk dat GraphQL meer inspanning vereist om in te stellen. Er zit dus wat waarheid in deze aanname.
Ik denk dat er een paar redenen voor zijn. Ten eerste omvat GraphQL (minimaal) twee onderdelen:
- het concept van wat het is en hoe het werkt
- de servers die een concrete implementatie bieden
Bij het doorlezen van documentatie voor GraphQL, zoals de officiële site graphql.org, richt het zich op de concepten achter GraphQL, met gedetailleerde informatie over resolvers, wat ze zijn en waarom ze nodig zijn.
Dit is nuttig als je een applicatie van de grond af opbouwt, zoals bij gebruik van Laravel en Lighthouse. In dat geval moet je inderdaad je resolvers coderen (maar dan zou je ook je REST-endpoints moeten maken).
WordPress is echter al de applicatie, en WPGraphQL en Gato GraphQL zijn oplossingen. Deze twee plugins hebben de resolvers al voor ons aangemaakt, zodat we ons er geen zorgen over hoeven te maken (vergelijkbaar met de WP REST API die ook een initiële set endpoints biedt, zodat we ons er geen zorgen over hoeven te maken).
Bovendien is GraphQL meer ontwikkelaarsgericht, en de documentatie lijkt rechtstreeks tot ontwikkelaars te spreken. Ontwikkelaars maken de resolvers aan de serverzijde, en ontwikkelaars gebruiken die resolvers met aangepaste queries aan de clientzijde. Omdat het bouwen van resolvers een taak voor ontwikkelaars is, komt het vanzelf en vaak ter sprake.
Bij REST is de verwachting (denk ik) dat het endpoint dat de vereiste data levert al bestaat (zoals geleverd door de WP REST API). Als het er niet is, pas dan hoeven we ons druk te maken over het instellen van een aangepast endpoint. Vandaar dat er minder nadruk ligt op het maken van resolvers voor REST.
Uiteindelijk leveren zowel REST als GraphQL de vereiste data. Maar terwijl REST een statische aanpak aanmoedigt, waarbij endpoints al zouden moeten bestaan en we ons er alleen zorgen over maken als ze er niet zijn, moedigt GraphQL een dynamische aanpak aan, waarbij elke query op maat is gemaakt en we de perfecte resolver ervoor kunnen coderen.
Er zijn dus uiteindelijk geen fundamentele verschillen tussen REST en GraphQL, alleen verschillende interpretaties van hoe ze aan hun vereisten moeten voldoen.
Kwetsbaarheden + Beveiligingsoverwegingen in GraphQL
We gaan ooit een enorme kwetsbaarheid zien door GraphQL, want het schrijven van veilige interpreters is echt moeilijk. [Source]
En...
WordPress is al zo groot dat het al een enorm doelwit op zijn rug heeft; het toevoegen van WELKE plugin dan ook brengt veel risico met zich mee, en een plugin die letterlijk heel WordPress blootstelt, inclusief veel codevoorbeelden voor het omzeilen van het beveiligingsmodel, is een groot nee voor mij. Uitvoer die niet door het thema wordt aangestuurd, moet zo beperkt mogelijk zijn (niet-bestaand tenzij ik erom vraag) buiten wat absoluut noodzakelijk is om bloot te stellen. Ik hoop dat dit nooit in de kern terechtkomt. [Source]
GraphQL brengt inderdaad extra beveiligingsrisico's met zich mee waar we mee om moeten gaan. Ik ben het volledig eens met dit gevoel.
Maar ik denk niet dat het zo'n blokkerend probleem is dat het een potentiële opname van GraphQL in WP-kern zou verhinderen. Sterker nog, ik denk niet eens dat het echt moeilijk aan te pakken is.
Wat nodig is, is dat de GraphQL-server gebruik maakt van de bestaande beveiligingsmechanismen van WordPress, en dat de ontwikkelaar deze mechanismen gebruikt om ervoor te zorgen dat bepaalde velden alleen toegankelijk zijn voor de juiste gebruikers:
- is de gebruiker ingelogd?
- is de gebruiker de beheerder?
- heeft de gebruiker een bepaalde rol of bevoegdheid?
- is de gebruiker de auteur van de post?
Om aan dit voorstel te voldoen, biedt Gato GraphQL Toegangscontrolelijsten, zodat we via configuratie kunnen definiëren wie toegang heeft tot elk veld en elke directive.
Soms is het gebruik van een ACL alleen niet voldoende en moet de GraphQL-server extra beveiligingsmaatregelen bieden. Ik zal beschrijven waar ik momenteel aan werk voor de aankomende v0.8 van Gato GraphQL.
Het veld posts (om postdata op te halen) vereist geen autorisatie, elke gebruiker kan er toegang toe krijgen, ingelogd of niet. Daarom haalt het uit veiligheidsoverwegingen alleen gepubliceerde posts op.
Maar er zijn situaties waarin we ook concept-/in-behandeling/verwijderde posts moeten ophalen, zoals:
- Voor het bouwen van een statische website, uitgevoerd door de beheerder, met toegang tot alle data van de site
- Voor auteurs van de post, om alle concepten te tonen zodat ze deze kunnen blijven bewerken
Ik heb vervolgens het volgende schema bedacht. Om posts op te halen, zijn er 3 velden:
posts: open voor iedereen, kan alleen gepubliceerde posts ophalenmyPosts: open voor iedereen, haalt alleen posts op van de ingelogde gebruiker, met elke status (gepubliceerd/concept/in behandeling/verwijderd)postsForAdmin: alleen de beheerder heeft toegang, haalt elke post op met elke status
En vervolgens is postsForAdmin standaard uitgeschakeld, zodat het niet eens verschijnt in het GraphQL-schema, tenzij de beheerder het expliciet inschakelt (en hoogstwaarschijnlijk wordt het alleen ingeschakeld voor het bouwen van statische sites).
Een andere situatie is wanneer een veld zowel publieke als privédata kan ophalen. Het veld option haalt bijvoorbeeld data op uit de tabel wp_options. Sommige vermeldingen zijn publiek (zoals blogname), terwijl andere dat niet zijn (zoals admin_email).
Een vergelijkbare situatie geldt voor het ophalen van meta-waarden, via de velden Post.metaValue, User.metaValue en andere. Zo bevat gebruikersmeta de vermelding wp_capabilities, die zeker privé is, terwijl description publiek is. En dan is er last_name, dat afhankelijk van de applicatie publiek of privé kan zijn.
Om toegang tot deze data veilig te maken, zal de plugin het mogelijk maken om te specificeren welke vermeldingen kunnen worden opgevraagd via een allow/denylist op de instellingenpagina, waarbij zowel de volledige vermelding als een regex wordt geaccepteerd:

Het opvragen van de toegestane optie werkt dan, terwijl de geweigerde optie gewoon null retourneert:
{
# This option is allowed
siteName: optionValue(name: "blogname")
# This optionValue is not allowed
adminEmail: optionValue(name: "admin_email")
}Met de juiste beveiligingsmaatregelen van de GraphQL-server en gezond verstand van de ontwikkelaar zou het maken van een veilige GraphQL API niet moeilijk moeten zijn.
GraphQL dat de database overbelast
GraphQL is een rijke syntaxis die het mogelijk maakt diepe relationele queries uit te drukken, dus voor een ecosysteem als WordPress, waar de uitbreidbaarheid van het datamodel voortkomt uit het entity-attribute-value-patroon, vertaalt dit zich in ongelooflijke hoeveelheden slijtage op een database, wat ertoe kan leiden dat je site niet meer reageert als de GraphQL-query diep, gecompliceerd of recursief is. WordPress staat al bekend om het kunnen platleggen van een MySQL/MariaDB-instantie, dus het toevoegen van GraphQL zou dit veel erger kunnen maken als de queries niet goed zijn geschreven, geverifieerd en snelheidsbeperkt. [Source]
De database overbelasten is een serieuze zorg voor GraphQL-servers. Ik zal beschrijven hoe Gato GraphQL probeert dit scenario te vermijden.
Gato GraphQL voorkomt dat het N+1-probleem zich ooit voordoet, al door architecturaal ontwerp. Het bereikt dit doordat de engine verantwoordelijk is voor het laden van entiteiten uit de database, niet de ontwikkelaar.
Bij het oplossen van verbindingen in een resolver is de geretourneerde waarde het ID (of de lijst van ID's) van het object of de objecten, en niet het object zelf. Het ophalen van de auteur van de aangepaste post wordt bijvoorbeeld zo gedaan:
class CustomPostFieldResolver extends AbstractDBDataFieldResolver
{
private CustomPostUserTypeAPIInterface $customPostUserTypeAPI;
public function getClassesToAttachTo(): array
{
return [
CustomPostFieldInterfaceResolver::class,
];
}
public function getSchemaFieldType(string $fieldName): ?string
{
return match($fieldName) {
'author' => SchemaDefinition::TYPE_ID,
default => null,
};
}
public function resolveValue(
TypeResolverInterface $typeResolver,
object $customPost,
string $fieldName,
array $fieldArgs = []
): mixed {
switch ($fieldName) {
case 'author':
return $this->customPostUserTypeAPI->getAuthorID($customPost);
}
return null;
}
public function resolveFieldTypeResolverClass(
TypeResolverInterface $typeResolver,
string $fieldName
): ?string {
switch ($fieldName) {
case 'author':
return UserTypeResolver::class;
}
return null;
}
}Met het ID van de database-entiteit uit resolveValue en het type van het object uit resolveFieldTypeResolverClass (vertegenwoordigd via klasse UserTypeResolver), kan de GraphQL-engine vervolgens de data voor het object laden.
Om de data te laden gebruikt de engine een algoritme dat superefficënt is: het heeft tijdcomplexiteit O(n), waarbij n het aantal types in de query is, niet het aantal knooppunten.
Het algoritme bereikt deze efficiëntie omdat het geen graaf doorloopt, maar de datastructuur converteert naar een stapel componenten, die veel eenvoudiger op te lossen is. (De "graph" in GraphQL is een concept, geen daadwerkelijke implementatie.)
Zelfs als de query meerdere niveaus heeft, waarbij elk niveau veel entiteiten ophaalt, kan het algoritme dit nog steeds vrij goed aan. Er is bijvoorbeeld geen grote impact bij het uitvoeren van de volgende query, die een diepte van 10 niveaus heeft:
{
posts(pagination: { limit: 10 }) {
excerpt
title
url
author {
name
url
posts(pagination: { limit: 10 }) {
title
tags(pagination: { limit: 10 }) {
slug
url
posts(pagination: { limit: 10 }) {
title
comments(pagination: { limit: 10 }) {
content
date
author {
name
posts(pagination: { limit: 10 }) {
title
url
comments(pagination: { limit: 10 }) {
content
date
author {
name
username
url
}
}
}
}
}
}
}
}
}
}
}De uitzondering op deze efficiëntie is bij het ophalen van meta-waarden via Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue en PostCategory.metaValue (en ook hun veld metaValues). Dat komt doordat de WordPress-functies (get_post_meta, get_user_meta, etc.) data ophalen voor 1 ID tegelijk, wat betekent dat elke entiteit een database-aanroep vereist om zijn meta-waarde op te halen. Als gevolg daarvan schaalt het oplossen van meta-waarden op basis van het aantal knooppunten, niet het aantal types (de opmerking van de OP raakt hier de spijker op z'n kop).
Om te voorkomen dat kwaadwillenden de meta-velden gebruiken en misbruiken, wordt Gato GraphQL (in v0.8) geleverd met deze velden standaard uitgeschakeld. De beheerder moet ze dan expliciet inschakelen en kan, terwijl hij dat doet, deze velden onder een Toegangscontrolelijst plaatsen, zodat de database op geen enkel moment aan een aanval blootgesteld is.
Snelheidsbeperking is ook een goed idee, ik plan dit te ondersteunen voor een toekomstige release.
En dan is er het analyseren en opleggen van beperkingen op de complexiteit van de query (zoals hoe veel niveaus diep het gaat). De GraphQL-server lost de query op met tijdcomplexiteit O(n), dus er is niet veel schade die kan worden aangericht wat betreft lussen. Een enkele query zou echter nog steeds onbeperkte hoeveelheden data uit de database kunnen ophalen, en dat is iets wat we wellicht willen vermijden.
Deze eenvoudige query zal bijvoorbeeld een enorme hoeveelheid data in één verzoek ophalen (mijn demosite heeft nauwelijks een paar honderd records, dus ik kan het me veroorloven om de uitvoering van de query te demonstreren):
{
posts000: posts(pagination: { limit: 100 }) {
...PostFields
}
posts100: posts(pagination: { limit: 100, offset: 100 }) {
...PostFields
}
posts200: posts(pagination: { limit: 100, offset: 200 }) {
...PostFields
}
posts300: posts(pagination: { limit: 100, offset: 300 }) {
...PostFields
}
posts400: posts(pagination: { limit: 100, offset: 400 }) {
...PostFields
}
posts500: posts(pagination: { limit: 100, offset: 500 }) {
...PostFields
}
posts600: posts(pagination: { limit: 100, offset: 600 }) {
...PostFields
}
posts700: posts(pagination: { limit: 100, offset: 700 }) {
...PostFields
}
posts800: posts(pagination: { limit: 100, offset: 800 }) {
...PostFields
}
posts900: posts(pagination: { limit: 100, offset: 900 }) {
...PostFields
}
}
fragment PostFields on Post {
id
title
content
date
}Zoals te zien is, hoeft de query niet eens genest te zijn om problemen te veroorzaken. Het analyseren van de complexiteit van een query is dan ook een lastige kwestie, die nauwkeurig afstemmen vereist om nuttig te zijn.
Ik hoop ook query-analyse te ondersteunen, maar het staat niet bovenaan mijn lijst van hoge prioriteiten, omdat we met een combinatie van de andere functies (zoals persistente queries of aangepaste endpoints, gecombineerd met Toegangscontrolelijsten) de kwaadwillenden al buiten de deur kunnen houden, en we zelf onze eigen GraphQL-service niet zullen (mogen!) misbruiken.