👶🏻 WordPress durch GraphQL verjüngen
WordPress ist ein veraltetes CMS: Es wurde vor mehr als 17 Jahren erfunden und ist voll mit PHP-Code, der – wenn man die Chance hätte, neu anzufangen – anders geschrieben werden würde.
GraphQL ist eine moderne Schnittstelle für den Datenzugriff. Beachte das Wort „Schnittstelle": Es spielt keine Rolle, wie das zugrundeliegende Datensystem implementiert ist, sondern nur, wie die Daten exponiert werden.
Was passiert, wenn wir diese beiden zusammenbringen? Wie sollten wir die GraphQL-Schnittstelle gestalten, um auf Daten aus WordPress zuzugreifen?
Es gibt ein paar naheliegende Strategien, die wir verfolgen können:
-
Die Tradition respektieren und ein Mapping bereitstellen, das das WordPress-Datenmodell so beibehält, wie es ist, einschließlich der technischen Schulden, die sich über die Jahre angesammelt haben
-
Die technischen Schulden beheben und eine Schnittstelle bereitstellen, die Daten abstrakt und nicht zwingend an WordPress gebunden exponiert
Beide Ansätze haben Vor- und Nachteile, und es gibt kein Richtig oder Falsch. Es ist einfach eine Frage der Präferenzen – bestimmte Verhaltensweisen gegenüber anderen zu priorisieren.
Für das Plugin Gato GraphQL habe ich den zweiten Ansatz gewählt und versucht, ein GraphQL-Schema zu erstellen, das zwar auf WordPress basiert und für WordPress funktioniert, aber nicht an WordPress gebunden ist (zum Beispiel durch das Entfernen inkonsistenter Namen und Beziehungen).
Das Ergebnis ist, dass GraphQL WordPress verjüngt: Obwohl wir WordPress weiterhin als unser zugrundliegendes CMS haben, mit seinem veralteten PHP-Code, kann seine Datenschicht neu aufgebaut werden – auf der Grundlage von gesundem Menschenverstand, nicht von Tradition. Die Datenschicht kehrt davon zurück, ein Teenager zu sein, und wird wieder zum Kleinkind.

Das Ergebnis ist ein GraphQL-Schema, das das WordPress-Datenmodell repräsentiert und auch verschachtelte Mutations unterstützt.
Schauen wir uns an, wie das umgesetzt wurde.
Das WordPress-Datenmodell
WordPress verfügt über folgende Entitäten:
- Posts
- Seiten
- Custom Posts
- Medienelemente
- Benutzer
- Benutzerrollen
- Tags
- Kategorien
- Kommentare
- Blöcke
- Meta-Eigenschaften
- Sonstiges (Optionen, Plugins, Themes, usw.)
Diese Entitäten können eine Hierarchie haben. Zum Beispiel sind Post, Seite und Medienelemente allesamt Custom Post Types, und Tags und Kategorien sind beide Taxonomien.
Dies ist das WordPress-Datenbankdiagramm, das zeigt, wie Daten für alle Entitäten gespeichert werden:

Ist das Mapping eine exakte Kopie des DB-Diagramms?
Wenn wir die WordPress-Datenbank in ein GraphQL-Schema mappen, wird dasselbe Diagramm oben 1:1 eingehalten?
Nein, das ist es nicht. Während das Datenbankdiagramm eine tatsächliche Implementierung ist, ist GraphQL eine Schnittstelle, um vom Client aus auf Daten zuzugreifen. Diese beiden sind verwandt, können aber unterschiedlich sein. GraphQL kümmert sich nicht um die Datenbank: Es denkt nicht in SQL-Befehlen und weiß nicht, dass es Datenbanktabellen namens wp_posts und wp_users gibt.
Wir müssen uns also beim Erstellen des GraphQL-Schemas für WordPress nicht zu sehr um das Datenbankdiagramm sorgen. Das bedeutet, dass wir ein GraphQL-Schema erstellen können, das einige der technischen Schulden des WordPress-Datenmodells behebt.
Das WordPress-Datenmodell als GraphQL-Schema mappen
Führen wir das Mapping durch. Zuerst mappen wir die ursprünglichen Entitäten so weit wie möglich als Typen. Aus der Liste der Entitäten im WordPress-Datenmodell erstellen wir folgende Typen für das GraphQL-Schema:
PostPageMediaUserUserRolePostTagPostCategoryComment
Dann fügen wir jedem Typ alle erwarteten Felder hinzu. Um das Schema darzustellen, können wir die SDL, die Schema Definition Language, verwenden. (Dies dient nur zu Dokumentationszwecken; das Plugin selbst verwendet die SDL nicht zur Kodierung des Schemas: Es ist alles PHP-Code).
Dies sind die Felder (unter vielen anderen) für einen Post:
type Post {
id: ID!
title: String
content: String
excerpt: String
publishedAt: Date!
}Dies sind die Felder (unter vielen anderen) für einen User:
type User {
id: ID!
name: String
email: String!
}Wir erstellen auch die entsprechenden Verbindungen, also Felder, die eine andere Entität zurückgeben (anstatt eines Skalars, wie einer Zahl oder einer Zeichenkette). Zum Beispiel stellen wir dar, dass ein Post einen Autor hat und ein Benutzer Posts besitzt:
type Post {
author: User!
}
type User {
posts: [Post]
}Felder und Verbindungen können auch Argumente akzeptieren. Zum Beispiel ermöglichen wir, dass Post.date formatiert wird, und dass User.posts Einträge sucht und ihre Anzahl begrenzt:
type Post {
date(format: String): Date!
}
type User {
posts(limit: Int, search: String): [Post]
}Wir machen das für alle Entitäten im WordPress-Datenmodell weiter. Wenn wir fertig sind, gelangen wir zum GraphQL-Schema für WordPress, das mit dem Voyager-Client sichtbar ist (im Menü des Plugins als „Interactive Schema" verfügbar):

Dieses Schema hat Ähnlichkeiten mit dem WordPress-Datenbankdiagramm, aber auch viele Unterschiede. Lass uns diese analysieren.
Operationen ohne Entität werden als Root-Felder gemappt
Das WordPress-Datenbankdiagramm stellt dar, wie Daten gespeichert werden, daher gibt es keinen „Anfang". GraphQL hingegen ist eine Schnittstelle zum Abrufen von Daten, daher muss es eine Ausgangsstufe geben, von der aus die Query ausgeführt wird.
Diese Ausgangsstufe ist der Root-Typ, oder genauer gesagt die Typen QueryRoot und MutationRoot (zur Handhabung von queries bzw. Mutations).
In diesen beiden Typen mappen wir alle Operationen, die nicht von einer Entität abhängen, wie beim Ausführen von get_posts(), get_users() oder wp_signon():
type QueryRoot {
posts: [Post]!
users: [User]!
}
type MutationRoot {
logUserIn(username: String, password: String): User
}Die Felder müssen nicht denselben Namen oder dieselbe Signatur wie die Operation haben, die sie repräsentieren. Zum Beispiel kann der Aufruf des Feldes logUserIn als passender als signOn angesehen werden.
Alle Mutations kommen unter MutationRoot
Es gibt Operationen, die von einer Entität abhängen, wie wp_update_post(), die auf einem bestimmten Post angewendet wird. Die entsprechende Mutation im GraphQL-Schema muss dem Typ MutationRoot hinzugefügt werden, weil das so die Funktionsweise von GraphQL ist.
Diese Operation wird dann folgendermaßen gemappt:
type MutationRoot {
updatePost(input: {
postID: ID!,
newTitle: String,
newContent: String
}): Post
}Dieses Plugin unterstützt auch verschachtelte Mutations, die als Opt-in-Funktion angeboten werden (weil dies kein Standard-GraphQL-Verhalten ist). Mutations können dann auch unter jedem Typ hinzugefügt werden, nicht nur unter MutationRoot. In diesem Fall erhalten wir:
type Post {
update(input: {
newTitle: String,
newContent: String
}): Post!
}Umgang mit Custom Posts
In GraphQL gibt es keine Typvererbung. Daher können wir keinen Typ CustomPost haben und deklarieren, dass Post und Page ihn erweitern.
GraphQL bietet zwei Mittel, um diesen Mangel auszugleichen: Interfaces und Union Types.
Für das erste erstellen wir ein Interface CustomPost für das Schema, das alle erwarteten Felder eines Custom Posts deklariert, und wir definieren die Typen Post und Page so, dass sie das Interface implementieren:
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!
}Für das zweite erstellen wir einen CustomPostUnion-Typ für das Schema, der alle Custom Post Types zurückgibt:
union CustomPostUnion = Post | PageUnd wir sorgen dafür, dass Felder diesen Typ zurückgeben, wann immer es angemessen ist:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Wie zu sehen ist, müssen wir im GraphQL-Schema explizit angeben, wann wir es mit Posts und wann mit Custom Posts zu tun haben, da diese nicht dasselbe sind! Diese beiden austauschbar zu nennen ist technische Schuld von WordPress, die wir beheben können.
Aus diesem Grund wird ein Custom Post immer CustomPost und nicht Post genannt, ein Feld, das Custom Posts behandelt, wird immer customPosts und nicht posts genannt, und ein Feldargument, das die ID für einen Custom Post empfängt, wird customPostID und nicht postID genannt (obwohl es so in der gemappten WordPress-Funktion heißt).
Die Erwartung ist dann immer klar:
- Feld
User.customPostskann eine Liste beliebiger Custom Posts zurückgeben, einschließlich Posts und Seiten, währendUser.postsnur Posts zurückgibt - Feld
Root.setFeaturedImageOnCustomPostkann einem beliebigen Custom Post ein Beitragsbild hinzufügen, deshalb heißt es nichtsetFeaturedImageOnPost
Tags (und Kategorien) nicht unter einem einzigen Typ gruppieren
Warum heißt der Typ PostTag (und dasselbe gilt für PostCategory) so und nicht einfach Tag?
Weil beim Ausführen dieser Query (bei der ein Produkt ein CPT ist) die Ergebnisse des Feldes tags für Posts und Produkte immer unterschiedlich und nicht überlappend sein werden:
query {
posts {
tags {
id
name
}
}
products {
tags {
id
name
}
}
}Tags, die Posts hinzugefügt wurden, erscheinen nicht beim Abrufen von Tags für Produkte und umgekehrt (es sei denn, ein Produkt verwendet auch die Taxonomie post_tag, aber dann kann es auch mit dem Typ PostTag dargestellt werden). Das ist in WordPress kein großes Problem, da diese Elemente als unterschiedliche Zeilen derselben Datenbanktabelle betrachtet werden können. Aber für GraphQL, das stark typisiert ist, spielt es eine Rolle.
Es ist daher eine gute Designentscheidung, diese Entitäten getrennt zu halten, unter ihren eigenen Typen, und Tags für Posts unter dem Typ PostTag zurückzugeben. Wenn ein benutzerdefiniertes Plugin seinen eigenen Produkt-CPT implementiert, muss es den Typ ProductTag für seine Tags verwenden.
Medienelementen eine eigene Identität geben
Medienentitäten in WordPress sind Custom Post Types, nur weil es aus Implementierungssicht praktisch war. Das GraphQL-Schema kann diese technische Schuld jedoch vermeiden und Medienelemente als eigenständige Entität modellieren, nicht als Custom Posts.
Dies hat folgende Konsequenzen für das GraphQL-Schema:
- Beim Abfragen des Feldes
customPostswerden keine Medienelemente abgerufen - Der Typ
Mediaimplementiert nicht das InterfaceCustomPostund ist nicht Teil des TypsCustomPostUnion - Der Typ
Mediahat nicht viele der Felder, die von einem Custom Post Type erwartet werden, wieexcerpt,dateundstatus. Stattdessen hat er nur die Felder, die von einem Medienelement erwartet werden:
type Media {
id: ID!
src: String!
width: Int
height: Int
}Enums identifizieren und mappen
In manchen Situationen verwendet WordPress feste Werte aus einer bestimmten Menge. Zum Beispiel kann der Status eines Posts nur "publish", "draft", "pending" oder "trash" sein.
In GraphQL können wir diese als Enums (anstatt als Strings) behandeln und einen entsprechenden Enumerationstyp erstellen. Nach dem GraphQL-Standard sollten Enums in Großbuchstaben geschrieben werden, wie folgt:
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}Dann kann die Query jedoch nicht direkt für die Interaktion mit WordPress verwendet werden, da das Ausführen von get_posts( [ "post_status" => "PUBLISH" ] ) nicht funktioniert.
Als Kompromiss behalten wir diese Enum-Werte daher in Kleinbuchstaben:
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Zusätzliche Typen mappen
Blöcke sind im WordPress-Datenbankdiagramm nicht direkt sichtbar, da sie in wp_posts gespeichert werden (es gibt keine Tabelle wp_blocks), aber dennoch sind sie eine eigenständige Entität.
Daher führen wir den Typ Block ein, um sie zu mappen:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}