👶🏻 WordPress verjongen via GraphQL
WordPress is een legacy-CMS: het bestaat al meer dan 17 jaar en is gevuld met PHP-code die, als je het opnieuw mocht doen, heel anders geschreven zou worden.
GraphQL is een moderne interface om data op te halen. Let op het woord "interface": het maakt niet uit hoe het onderliggende datasysteem is geïmplementeerd, alleen hoe de data wordt blootgesteld.
Wat gebeurt er als we deze twee combineren? Hoe moeten we de GraphQL-interface ontwerpen om data uit WordPress op te halen?
Er zijn een paar voor de hand liggende strategieën die we kunnen toepassen:
-
De traditie respecteren en een mapping bieden die het WordPress-datamodel ongewijzigd behoudt, inclusief de technische schuld die het door de jaren heen heeft opgebouwd
-
De technische schuld oplossen door een interface te bieden die data op een abstracte, niet per se aan WordPress gebonden manier blootstelt
Beide benaderingen hebben voor- en nadelen, en geen ervan is goed of fout. Het is gewoon een kwestie van voorkeur — sommig gedrag boven ander stellen.
Voor de plugin Gato GraphQL heb ik de tweede benadering gekozen, met als doel een GraphQL-schema te maken dat, hoewel het op WordPress is gebaseerd en voor WordPress werkt, niet aan WordPress gebonden is (bijvoorbeeld door inconsistente namen en relaties te verwijderen).
Het resultaat is dat GraphQL WordPress verjongt: we hebben nog steeds WordPress als ons onderliggende CMS, met zijn legacy PHP-code, maar de datalaag kan opnieuw worden opgebouwd, gebaseerd op gezond verstand in plaats van traditie. De datalaag gaat terug van een puber naar een peuter.

Het resultaat is een GraphQL-schema dat het WordPress-datamodel vertegenwoordigt, en dat ook geneste mutaties ondersteunt.
Laten we eens bekijken hoe dat is aangepakt.
Het WordPress-datamodel
WordPress heeft de volgende entiteiten:
- posts
- pagina's
- custom posts
- media-elementen
- gebruikers
- gebruikersrollen
- tags
- categorieën
- reacties
- blokken
- meta-eigenschappen
- overige (opties, plugins, thema's, etc.)
Deze entiteiten kunnen een hiërarchie hebben. Zo zijn post, pagina en media-elementen allemaal custom post types, en zijn tags en categorieën beide taxonomieën.
Dit is het databasediagram van WordPress, dat laat zien hoe de data voor alle entiteiten wordt opgeslagen:

Is de mapping een exacte kopie van het DB-diagram?
Als we de WordPress-database naar een GraphQL-schema mappen, wordt het bovenstaande diagram dan 1 op 1 gevolgd?
Nee, dat is niet zo. Terwijl het databasediagram een daadwerkelijke implementatie is, is GraphQL een interface om data vanuit de client op te halen. Beide zijn gerelateerd, maar kunnen verschillen. GraphQL maakt zich geen zorgen over de database: het denkt niet in SQL-opdrachten en weet niet dat er databasetabellen bestaan die wp_posts en wp_users heten.
We hoeven ons dus niet al te druk te maken over het databasediagram bij het maken van het GraphQL-schema voor WordPress. Dat betekent dat we een GraphQL-schema kunnen produceren dat een deel van de technische schuld van het WordPress-datamodel oplost.
Het WordPress-datamodel mappen als GraphQL-schema
Laten we de mapping uitvoeren. Eerst mappen we de oorspronkelijke entiteiten zoveel mogelijk als types. Uit de lijst van entiteiten in het WordPress-datamodel produceren we de volgende types voor het GraphQL-schema:
PostPageMediaUserUserRolePostTagPostCategoryComment
Vervolgens voegen we alle verwachte velden toe aan elk type. Om het schema te representeren kunnen we de SDL gebruiken, oftewel de Schema Definition Language. (Dit is alleen voor documentatiedoeleinden; de plugin zelf gebruikt geen SDL om het schema vast te leggen: het is allemaal PHP-code).
Dit zijn de velden (onder andere) voor een Post:
type Post {
id: ID!
title: String
content: String
excerpt: String
publishedAt: Date!
}Dit zijn de velden (onder andere) voor een User:
type User {
id: ID!
name: String
email: String!
}We maken ook de bijbehorende verbindingen, dat zijn velden die een andere entiteit teruggeven (in plaats van een scalaire waarde, zoals een getal of een string). Zo representeren we dat een post een auteur heeft en dat een gebruiker posts bezit:
type Post {
author: User!
}
type User {
posts: [Post]
}Velden en verbindingen kunnen ook argumenten accepteren. Zo stellen we Post.date in staat om geformatteerd te worden, en User.posts om entries te doorzoeken en hun aantal te beperken:
type Post {
date(format: String): Date!
}
type User {
posts(limit: Int, search: String): [Post]
}We doen dit voor alle entiteiten in het WordPress-datamodel. Als we klaar zijn, komen we uit op het GraphQL-schema voor WordPress, dat zichtbaar is via de Voyager-client (beschikbaar als "Interactive Schema" in het menu van de plugin):

Dit schema vertoont overeenkomsten met het WordPress-databasediagram, maar ook veel verschillen. Laten we die analyseren.
Operaties zonder entiteit worden als Root-velden gemapped
Het WordPress-databasediagram geeft aan hoe data wordt opgeslagen, dus er is geen "beginpunt". GraphQL is echter een interface om data op te halen, dus er moet een beginstadium zijn vanwaaruit de query wordt uitgevoerd.
Dit beginstadium is het Root-type, of preciezer gezegd, de types QueryRoot en MutationRoot (voor het afhandelen van respectievelijk queries en mutaties).
In deze twee types mappen we alle operaties die niet afhankelijk zijn van een entiteit, zoals het uitvoeren van get_posts(), get_users() of wp_signon():
type QueryRoot {
posts: [Post]!
users: [User]!
}
type MutationRoot {
logUserIn(username: String, password: String): User
}De velden hoeven niet dezelfde naam of signatuur te hebben als de operatie die ze vertegenwoordigen. Het veld logUserIn kan bijvoorbeeld als passender worden beschouwd dan signOn.
Alle mutaties gaan onder MutationRoot
Er zijn operaties die wel afhankelijk zijn van een entiteit, zoals wp_update_post(), die wordt toegepast op een bepaalde post. De bijbehorende mutatie in het GraphQL-schema moet worden toegevoegd aan het type MutationRoot, omdat GraphQL zo werkt.
Deze operatie wordt dan als volgt gemapped:
type MutationRoot {
updatePost(input: {
postID: ID!,
newTitle: String,
newContent: String
}): Post
}Deze plugin ondersteunt ook geneste mutaties, aangeboden als een opt-in functie (omdat dit geen standaard GraphQL-gedrag is). Mutaties kunnen dan ook worden toegevoegd onder elk type, niet alleen onder MutationRoot. In dat geval krijgen we:
type Post {
update(input: {
newTitle: String,
newContent: String
}): Post!
}Omgaan met custom posts
Er is geen type-overerving in GraphQL. We kunnen dus geen type CustomPost hebben en verklaren dat Post en Page dit uitbreiden.
GraphQL biedt twee middelen om dit gebrek te compenseren: interfaces en union-types.
Voor het eerste maken we een interface CustomPost voor het schema, waarin we alle velden declareren die van een custom post worden verwacht, en we definiëren de types Post en Page om de interface te implementeren:
interface CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Post implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Page implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}Voor het tweede maken we een type CustomPostUnion voor het schema dat alle custom post types teruggeeft:
union CustomPostUnion = Post | PageEn laten we velden dit type teruggeven waar dat van toepassing is:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Zoals te zien is, moeten we in het GraphQL-schema expliciet aangeven wanneer we met posts werken en wanneer met custom posts, want dat zijn niet hetzelfde! Deze twee door elkaar gebruiken is technische schuld van WordPress die we kunnen oplossen.
Daarom wordt een custom post altijd CustomPost genoemd en niet Post, wordt een veld dat met custom posts werkt altijd customPosts genoemd en niet posts, en wordt een veldargument dat het ID van een custom post ontvangt customPostID genoemd en niet postID (ook al heet het zo in de gemapte WordPress-functie).
De verwachting is dan altijd duidelijk:
- veld
User.customPostskan een lijst van welke custom post dan ook teruggeven, inclusief posts en pagina's, terwijlUser.postsalleen posts teruggeeft - veld
Root.setFeaturedImageOnCustomPostkan een uitgelichte afbeelding toevoegen aan elke custom post, daarom heet het nietsetFeaturedImageOnPost
Tags (en categorieën) niet groeperen onder één type
Waarom heet het type PostTag (en hetzelfde geldt voor PostCategory) zo, in plaats van gewoon Tag?
Omdat, wanneer je deze query uitvoert (waarbij een product een CPT is), de resultaten van het veld tags voor posts en producten altijd verschillend en niet-overlappend zijn:
query {
posts {
tags {
id
name
}
}
products {
tags {
id
name
}
}
}Tags die aan posts zijn toegevoegd, verschijnen niet bij het ophalen van tags voor producten, en omgekeerd (tenzij een product ook de taxonomie post_tag gebruikt, maar dan kan het ook worden weergegeven met het type PostTag). In WordPress is dit geen groot probleem, omdat deze items kunnen worden beschouwd als verschillende rijen uit dezelfde databasetabel. Maar voor GraphQL, dat sterk getypeerd is, maakt het wel uit.
Het is dan ook een goede ontwerpbeslissing om deze entiteiten gescheiden te houden, elk onder hun eigen type, waarbij tags voor posts worden teruggegeven onder het type PostTag, en als een aangepaste plugin zijn eigen product-CPT implementeert, het type ProductTag moet gebruiken voor zijn tags.
Media-items een eigen identiteit geven
Media-entiteiten in WordPress zijn custom post types, simpelweg omdat dat implementatietechnisch handig was. Het GraphQL-schema kan deze technische schuld echter vermijden en media-elementen modelleren als een aparte entiteit, niet als custom posts.
Dit impliceert de volgende beslissingen voor het GraphQL-schema:
- Bij het opvragen van het veld
customPostsworden geen media-elementen opgehaald - Het type
Mediaimplementeert de interfaceCustomPostniet en maakt geen deel uit van het typeCustomPostUnion - Het type
Mediaheeft niet veel velden die van een custom post type worden verwacht, zoalsexcerpt,dateenstatus. In plaats daarvan heeft het alleen de velden die van een media-element worden verwacht:
type Media {
id: ID!
src: String!
width: Int
height: Int
}Enums identificeren en mappen
In bepaalde situaties gebruikt WordPress vaste waarden uit een bepaalde set. Zo kan de status van een post alleen "publish", "draft", "pending" of "trash" zijn.
In GraphQL kunnen we deze behandelen als enums (in plaats van strings) en een bijbehorend enum-type aanmaken. Conform de GraphQL-standaard moeten enums in hoofdletters worden geschreven, als volgt:
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}Dan kan de query echter niet direct worden gebruikt om met WordPress te communiceren, omdat het uitvoeren van get_posts( [ "post_status" => "PUBLISH" ] ) niet werkt.
Als compromis houden we deze enum-waarden dan ook in kleine letters:
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Extra types mappen
Blokken zijn niet direct zichtbaar in het WordPress-databasediagram, omdat ze worden opgeslagen in wp_posts (er is geen tabel wp_blocks), maar het zijn toch afzonderlijke entiteiten.
Daarom introduceren we het type Block om ze te mappen:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}