Concepten, Ideeën, Strategieën
Concepten, Ideeën, StrategieënHet GraphQL-schema in kaart brengen voor je WordPress-site, thema of plugin

Het GraphQL-schema in kaart brengen voor je WordPress-site, thema of plugin

Je hebt besloten om GraphQL te gaan gebruiken voor je bestaande WordPress-site. Geweldig! Of het nu gaat om nieuwe of bestaande functionaliteit, GraphQL moet samenwerken met de onderliggende datalaag. Daarvoor moet je het datamodel van je applicatie (of het nu gaat om aangepaste PHP-code op je WordPress-site, je thema of je plugin) in kaart brengen in het GraphQL-schema.

Hoe moet die mapping worden gedaan? Moet het allemaal tegelijk? Moet het een exacte kopie zijn van het bestaande datamodel? Wat als je daarbij een ongeschikte naam wil corrigeren? En wat betreft technische schuld — moet die worden behouden of aangepakt?

Laten we een aantal strategieën verkennen voor het in kaart brengen van het datamodel van een bestaande WordPress-applicatie in een GraphQL-schema.

Breng het schema in je eigen tempo in kaart

GraphQL toevoegen aan een applicatie is geen kwestie van alles of niets. Dezelfde applicatie kan tegelijkertijd door meerdere API's worden aangestuurd, waarbij GraphQL naast andere API's kan bestaan zo lang als nodig. We kunnen bijvoorbeeld de bestaande functionaliteit op REST laten draaien en GraphQL alleen inzetten voor alle nieuwe functionaliteit.

Als je een volledige migratie naar GraphQL wilt uitvoeren, hoeft dat niet allemaal tegelijk. De bestaande functionaliteit kan langzaam maar gestaag naar GraphQL worden gemigreerd, totdat GraphQL op een dag de enige API in de applicatie is.

Hoewel je op dag 1 al een volledig GraphQL-schema zou kunnen aanmaken, hoeft dat dus niet: op elk moment hoeven alleen die entiteiten aanwezig te zijn in het schema die door de functionaliteit vereist worden (via hun typen, velden en interfaces). Je kunt ze stapsgewijs in kaart brengen, op een progressieve manier.

Laat de interface de last van de implementatie niet dragen

De GraphQL-server implementeert de logica om toegang te krijgen tot de applicatiedata. Dit doet hij door WordPress-functionaliteit aan te roepen, zoals het aanroepen van get_posts om postdata op te halen. Op deze laag staat PHP-code om de resolvers te voorzien.

Een GraphQL-schema is echter een interface: het declareert de contracten voor toegang tot data in de API. Het houdt zich niet bezig met implementatiedetails: het weet niets over WordPress, over de functie get_posts, de databasetabel wp_posts of SQL-queries.

Daarom moeten we informatie-lekkage tussen de lagen zo veel mogelijk vermijden.

Dit is belangrijk, omdat het datamodel vaak besmet zal zijn door de implementatie ervan. WordPress biedt hiervan een duidelijk voorbeeld met het "attachment" CPT, om mediabestanden zoals afbeeldingen te vertegenwoordigen.

Omdat het een Custom Post Type is, wordt een afbeelding behandeld als een post. We kunnen dan in de verleiding komen om mediabestanden te vertegenwoordigen via het type Post, dat deze velden bevat:

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
}

Maar dit is misschien niet geschikt voor de applicatie. De betekenis van het veld "content" is duidelijk voor een post, maar niet voor een afbeelding. Het zou daar hoogstwaarschijnlijk niet bij horen.

Een afbeelding werd in WordPress als CPT gemodelleerd omdat het handig was, zodat het bestaande logica kon hergebruiken en kon worden opgeslagen in de bestaande tabel wp_posts.

Handig betekent echter niet correct, en kan uiteindelijk leiden tot technische schuld (d.w.z. gebrekkige code die niet kan worden hersteld zonder een breaking change te introduceren, waardoor deze langer in de applicatie blijft dan zou moeten).

We willen zo veel mogelijk technische schuld in onze applicatie vermijden. Wanneer we de kans krijgen, moeten we die aanpakken. Het in kaart brengen van het datamodel in het GraphQL-schema biedt zo'n gelegenheid, waarmee we het probleem kunnen corrigeren op de data-interface-laag.

(De technische schuld zal echter nog steeds bestaan op applicatieniveau, dus we lossen het probleem niet volledig op, maar verminderen het binnen onze mogelijkheden.)

Laten we dit idee in de praktijk brengen. In plaats van een type Post te laten staan voor mediabestanden, is het logischer om een type Media te hebben dat alleen die eigenschappen bevat die daadwerkelijk zinvol zijn voor een afbeeldingsentiteit:

type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Onder de motorkap zal de field resolver op implementatieniveau nog steeds de functie get_posts uitvoeren om vermeldingen van het type Media op te lossen, maar dat is geen zorg voor het GraphQL-schema.

Ontkoppel het GraphQL-schema van het databasediagram

WordPress is geïmplementeerd op basis van dit entiteit-relatiediagram van de database:

Entiteit-relatiediagram van de database in WordPress

Het GraphQL-schema moet gebaseerd zijn op het databasediagram, maar we moeten niet proberen een 1-op-1-kopie te maken. Dat komt omdat zowel het GraphQL-schema als het databasediagram zijn gebouwd met bepaalde voorwaarden of beperkingen, die niet op het andere van toepassing zijn.

De vorige sectie toont een voorbeeld waarbij tabel wp_posts de data opslaat voor het afbeeldings-CPT, maar in GraphQL zullen er twee afzonderlijke typen zijn: Post en Media.

Laten we een ander voorbeeld bekijken: categorieën. In WordPress kan een post een categorie (of meer) hebben, en elk CPT kan ook zijn eigen categorie aanmaken. Een CPT genaamd "event" zal bijvoorbeeld een "event_category" hebben.

Zowel postcategorieën als evenementcategorieën worden opgeslagen in tabel wp_terms. Dit maakt het voor WordPress eenvoudig om rijen op te halen uit het ene of het andere categorietype bij het uitvoeren van de SQL-query.

We kunnen dan in de verleiding komen om categorieën te mappen via het type Category, waarnaar zowel posts als evenementen verwijzen:

type Category {
  id: ID!
  name: String!
}
 
type Post {
  categories: [Category]!
}
 
type Event {
  categories: [Category]!
}

Een post zal echter altijd postcategorieën bevatten, en een evenement zal altijd evenementcategorieën bevatten. De data voor deze twee categorietypen kunnen worden opgeslagen in dezelfde databasetabel, maar ze worden op applicatieniveau niet door elkaar gehaald. Postcategorie en evenementcategorie zijn twee afzonderlijke entiteiten.

GraphQL heeft een statisch typesysteem. Om het meeste uit GraphQL te halen, moeten verschillende entiteiten op applicatieniveau worden gemodelleerd met behulp van verschillende typen in het GraphQL-schema.

In dit geval, bij het in kaart brengen van categorieën in het GraphQL-schema, moeten we voor elk een ander type aanmaken: PostCategory en EventCategory. Dan zal type Post alleen verwijzen naar PostCategory, en type Event alleen naar EventCategory:

type PostCategory {
  id: ID!
  name: String!
}
 
type Post {
  categories: [PostCategory]!
}
 
type EventCategory {
  id: ID!
  name: String!
}
 
type Event {
  categories: [EventCategory]!
}

Als we toch een entiteit in het schema willen hebben die alle categorieën omvat, kan dit worden bereikt via een interface Category:

interface Category {
  name: String!
}
 
type PostCategory implements Category {
  id: ID!
  name: String!
}
 
type EventCategory implements Category {
  id: ID!
  name: String!
}

Op deze manier hebben gebruikers die de API benaderen een duidelijk begrip van welke data er wordt opgehaald, ongeacht hoe het in het databasediagram is gemapt en hoe het in de database is opgeslagen.

Zodra we het definitieve GraphQL-schema hebben, kunnen we zien dat de vorm ervan enigszins lijkt op het WordPress-databasediagram, maar er duidelijk van verschilt:

GraphQL-schema

Pas de naamgeving van velden aan op basis van het statische typen

Velden moeten, zo veel mogelijk, dezelfde naam behouden als ze in de applicatie hebben.

We kunnen bijvoorbeeld een post aanmaken met de functie wp_insert_post, en de post heeft de eigenschappen "title" en "content". Deze namen zijn ook geschikt voor het GraphQL-schema (ook al hebben ze misschien kleine aanpassingen nodig), dus we moeten ze behouden:

type MutationRoot {
  insertPost(title: String, content: String): Post
}
 
type Post {
  id: ID!
  title: String
  content: String
}

Maar dat is niet altijd het geval. Zoals we eerder zagen, moeten custom posts worden ontkoppeld in hun eigen entiteiten. Terwijl de functie get_posts een lijst van elk CPT ophaalt, zal een equivalent veld posts in het roottype van het schema alleen entiteiten van het type Post ophalen, maar niet Page (dat ook een CPT is):

type QueryRoot {
  posts: [Post]!
}

Hoe krijgen we dan de lijst van alle posts en pagina's? Via een ander veld, customPosts, dat de entiteiten van elk CPT ophaalt dat is gemapt onder het union-type CustomPostUnion:

union CustomPostUnion = Post | Page
 
type QueryRoot {
  customPosts: [CustomPostUnion]!
}

De belangrijke les is deze: de naamgeving die we kiezen voor het GraphQL-schema moet worden aangepast aan het type van de opgehaalde entiteit. En vanwege de sterke typering van GraphQL kan dat type verschillend zijn op de applicatie- en API-lagen.

In dit geval, terwijl in WordPress een "post" elk "custom post type" kan betekenen, is een "post" in GraphQL noodzakelijkerwijs een Post. Als een veld custom posts ophaalt, moet het veld in het GraphQL-schema customPosts heten, niet posts. Op dezelfde manier moet een invoer die een ID voor een custom post ontvangt, customPostID heten, niet postID.

Mapping voor het veld customPosts

Deze les geldt bijvoorbeeld voor reacties. Een reactie kan aan elk CPT worden toegevoegd, niet alleen aan posts. Dan moet het type Comment dit duidelijk aangeven door het veld customPost te bevatten (en niet post):

type Comment {
  id: ID!
  customPost: CustomPostUnion!
}

Converteer vooraf gedefinieerde tekenreekswaarden naar enums, gebruik indien mogelijk hoofdletters

Opsommingstypen worden, naar conventie, in hoofdletters gedefinieerd. De documentatie van graphql.org geeft bijvoorbeeld dit voorbeeld:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Wanneer we een nieuw enum-type moeten aanmaken, moeten we hoofdletters gebruiken voor de gedefinieerde constanten. Maar bij het migreren van het datamodel vanuit de applicatie kunnen we bepaalde sets van vooraf gedefinieerde waarden tegenkomen die we via een enum kunnen mappen, maar waarvan de waarden tekenreeksen in kleine letters zijn.

Als voorbeeld: posts in WordPress hebben een eigenschap "status", die een van de volgende waarden bevat:

  • "publish"
  • "pending"
  • "draft"
  • "trash"

Bij het mappen van deze eigenschap in het schema zou het veld Post.status een String kunnen retourneren, zoals dit:

type Post {
  status: String!
}

Maar omdat de status noodzakelijkerwijs een van die vooraf gedefinieerde waarden zal zijn, en geen andere, geven we er de voorkeur aan het als een enum te mappen:

enum Status {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}
 
type Post {
  status: Status!
}

Nu kunnen we een probleem hebben: de enum PUBLISH wordt in de applicatie omgezet naar de tekenreekswaarde "PUBLISH", en niet "publish".

Door een waarde in hoofdletters te gebruiken in plaats van de verwachte in kleine letters, kan de logica in de applicatie verstoord raken. Inderdaad, het uitvoeren van de volgende code in WordPress werkt niet:

// Dit haalt alle posts op, niet alleen de gepubliceerde
$published_posts = get_posts([
  "post_status" => "PUBLISH",
]);

In dit geval kunnen we overwegen conventie in te ruilen voor gemak, en nog steeds een enum te gebruiken om de constanten te mappen, maar dan in kleine letters:

enum Status {
  publish
  draft
  pending
  trash
}

Met andere woorden, we kunnen een middenweg vinden tussen correct zijn en praktisch zijn. We moeten best practices toepassen bij het bouwen van het GraphQL-schema, maar mogen ervan afwijken wanneer dat zinvol is.