Wie das Plugin das WordPress-Datenmodell auf das GraphQL-Schema abbildet
So hat Gato GraphQL das WordPress-Datenmodell auf ein entsprechendes GraphQL-Schema abgebildet.
Das WordPress-Datenmodell
WordPress verfügt über folgende Entitäten:
- posts
- pages
- 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. So sind zum Beispiel Post, Page und Medienelemente allesamt custom post types, und Tags und Kategorien sind beide Taxonomien.
Das ist das WordPress-Datenbankdiagramm, das zeigt, wie die Daten aller Entitäten gespeichert werden:

Ist die Abbildung eine exakte Kopie des Datenbankdiagramms?
Wird beim Abbilden der WordPress-Datenbank auf ein GraphQL-Schema das oben gezeigte Diagramm exakt 1 zu 1 ĂĽbernommen?
Nein, das ist nicht der Fall. Während das Datenbankdiagramm eine tatsächliche Implementierung darstellt, ist GraphQL eine Schnittstelle zum Zugriff auf Daten vom Client aus. Beides hängt zusammen, kann 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 allzu sehr um das Datenbankdiagramm kümmern. Wir können sogar ein GraphQL-Schema erstellen, das einige der technischen Schulden des WordPress-Datenmodells behebt.
Das WordPress-Datenmodell als GraphQL-Schema abbilden
Führen wir die Abbildung durch. Zuerst bilden wir die ursprünglichen Entitäten so weit wie möglich als Typen ab. Aus der Liste der Entitäten im WordPress-Datenmodell erzeugen 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, also die Schema Definition Language, verwenden. (Das dient nur zur Dokumentation; das Plugin selbst verwendet SDL nicht zur Codierung des Schemas: alles ist PHP-Code).
Das sind die Felder (unter vielen anderen) fĂĽr einen Post:
type Post {
id: ID!
title: String
content: String
excerpt: String
date: Date!
}Das 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 einem String). Zum Beispiel bilden wir ab, 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.dateStr formatiert werden kann und User.posts Einträge filtern, deren Anzahl begrenzen und sie sortieren kann:
type Post {
dateStr(format: String): Date!
}
type User {
posts(
filter: RootPostsFilterInput
pagination: PostPaginationInput
sort: CustomPostSortInput
): [Post!]!
}
input RootPostsFilterInput {
authorIDs: [ID!]
authorSlug: String
categoryIDs: [ID!]
dateQuery: [DateQueryInput!]
excludeAuthorIDs: [ID!]
excludeIDs: [ID!]
hasPassword: Boolean = false
ids: [ID!]
isSticky: Boolean
metaQuery: [CustomPostMetaQueryInput!]
password: String
search: String
status: [FilterCustomPostStatusEnum!]
tagIDs: [ID!]
tagSlugs: [String!]
}
input PostPaginationInput {
limit: Int
offset: Int
}
input CustomPostSortInput {
by: CustomPostOrderByEnum
order: OrderEnum
}
# ...Das machen wir für alle Entitäten im WordPress-Datenmodell. Wenn wir fertig sind, erhalten wir das GraphQL-Schema für WordPress, das über den Voyager-Client einsehbar ist (im Menü des Plugins als "Interactive Schema" verfügbar):

Dieses Schema weist Ähnlichkeiten mit dem WordPress-Datenbankdiagramm auf, aber auch einige Unterschiede. Analysieren wir diese.
Operationen ohne Entität werden als Root-Felder abgebildet
Das WordPress-Datenbankdiagramm stellt dar, wie Daten gespeichert werden, daher gibt es keinen „Anfang". GraphQL hingegen ist eine Schnittstelle zum Abrufen von Daten, weshalb es eine Ausgangsphase geben muss, von der aus die Query ausgeführt wird.
Diese Ausgangsphase ist der Typ Root, oder genauer gesagt die Typen QueryRoot und MutationRoot (zur Verarbeitung von queries bzw. mutations).
In diesen beiden Typen bilden wir alle Operationen ab, 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 {
loginUser(
usernameOrEmail: String!,
password: String!
): User
}Die Felder müssen nicht denselben Namen oder dieselbe Signatur haben wie die Operation, die sie repräsentieren. Zum Beispiel kann das Feld loginUser als geeigneter angesehen werden als signOn.
Schemaelemente gruppieren
Wir können Verbesserungen vornehmen, um das Schema zu vereinfachen und nützlicher zu machen. Zum Beispiel kann ein Feld alle seine Argumente über ein Input-Objekt empfangen, das über mehrere Felder hinweg wiederverwendet werden kann und die Visualisierung des Schemas erleichtert:
type MutationRoot {
loginUser(input: LoginUserByInput!): User
}
input LoginUserByInput {
usernameOrEmail: String!,
password: String!
}Zusätzlich kann die Antwort einer Mutation ein „Payload"-Objekt sein, das neben dem zurückgegebenen betroffenen Objekt auch den Status der Operation und Fehlermeldungen enthalten kann:
type MutationRoot {
loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
type RootLoginUserMutationPayload {
errors: [RootLoginUserMutationErrorPayloadUnion!]
status: OperationStatusEnum!
user: User
userID: ID
}
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
| InvalidUserEmailErrorPayload
| InvalidUsernameErrorPayload
| PasswordIsIncorrectErrorPayload
| UserIsLoggedInErrorPayloadAlle Mutations kommen unter MutationRoot
Es gibt Operationen, die von einer Entität abhängen, wie wp_update_post(), die auf einen bestimmten Post angewendet wird. Die entsprechende Mutation im GraphQL-Schema muss dem Typ MutationRoot hinzugefügt werden, weil das der Funktionsweise von GraphQL entspricht.
Diese Operation wird dann so abgebildet:
type MutationRoot {
updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
input RootUpdatePostFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
id: ID!
status: CustomPostStatusEnum
tags: [String!]
title: String
}Dieses Plugin unterstützt auch verschachtelte Mutations, die als Opt-in-Funktion angeboten werden (da dies kein standardmäßiges GraphQL-Verhalten ist). Dann können Mutations auch unter jedem beliebigen Typ hinzugefügt werden, nicht nur unter MutationRoot. In diesem Fall erhalten wir:
type Post {
update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
input PostUpdateFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
status: CustomPostStatusEnum
tags: [String!]
title: String
}Beachte den Unterschied zwischen den Inputs RootUpdatePostFilterInput und PostUpdateFilterInput (also zwischen Mutations von der Root und verschachtelten Mutations): Ersterer hat die obligatorische Eigenschaft id, um anzugeben, welcher Post geändert werden soll, während Letzterer sie nicht hat, da er sie nicht benötigt.
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 Ressourcen, um diesen Mangel auszugleichen: Interfaces und Union-Typen.
FĂĽr Ersteres erstellen wir ein Interface CustomPost fĂĽr das Schema, das alle erwarteten Felder eines custom posts deklariert, und wir definieren die Typen Post, Page und GenericCustomPost (um alle custom post types darzustellen, die von einem installierten Theme oder Plugin definiert werden), sodass 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!
}
type GenericCustomPost implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}FĂĽr Letzteres erstellen wir einen Typ CustomPostUnion fĂĽr das Schema, der alle custom post types zurĂĽckgibt:
union CustomPostUnion = Post | Page | GenericCustomPostUnd wir sorgen dafĂĽr, dass Felder diesen Typ zurĂĽckgeben, wenn es angemessen ist:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Bei der Ausführung der Query können wir die Felder entweder basierend auf dem tatsächlichen Typ, z. B. Post, oder basierend auf dem Interface CustomPost auswählen:
{
customPosts {
__typename
...on CustomPost {
id
title
slug
status
}
...on Post {
isSticky
postFormat
}
}
}Wie zu erkennen ist, müssen wir im GraphQL-Schema explizit angeben, ob wir es mit posts oder mit custom posts zu tun haben, da diese nicht dasselbe sind! Diese beiden austauschbar zu verwenden ist eine technische Schuld von WordPress, die das Plugin nach Möglichkeit zu beheben versucht.
Aus diesem Grund wird ein custom post immer CustomPost genannt und nicht Post, ein Feld, das mit custom posts arbeitet, wird immer customPosts genannt und nicht posts, und ein Feldargument, das die ID eines custom posts empfängt, heißt customPostID und nicht postID (auch wenn das der Name in der abgebildeten WordPress-Funktion ist).
Damit ist die Erwartung immer klar:
- Das Feld
User.customPostskann eine Liste beliebiger custom posts zurückgeben, einschließlich posts und pages, währendUser.postsnur posts zurückgibt - Das 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 bei der AusfĂĽhrung dieser Query (wobei ein Produkt ein CPT ist) die Ergebnisse des Feldes tags fĂĽr posts und Produkte immer unterschiedlich und nicht ĂĽberschneidend 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 ebenfalls die Taxonomie post_tag, aber dann kann es auch mit dem Typ PostTag dargestellt werden). Das stellt in WordPress kein großes Problem dar, da diese Elemente als verschiedene Zeilen in derselben Datenbanktabelle betrachtet werden können. Für GraphQL ist es jedoch wichtig, da es stark typisiert ist.
Daher ist es eine gute Designentscheidung, diese Entitäten getrennt unter eigenen Typen zu halten, sodass Tags für posts unter dem Typ PostTag zurückgegeben werden, und wenn ein benutzerdefiniertes Plugin seinen eigenen CPT für Produkte 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 das 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.
Das bringt folgende Entscheidungen fĂĽr das GraphQL-Schema mit sich:
- Der Typ
Mediaimplementiert das InterfaceCustomPostnicht und wird nicht Teil des TypsCustomPostUnionsein - Der Typ
Mediahat viele Felder nicht, 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 abbilden
In einigen 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 (statt als Strings) behandeln und einen entsprechenden Enumerationstyp erstellen. Gemäß dem GraphQL-Standard sollten Enums in Großbuchstaben geschrieben werden, so:
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}Dann kann die Query jedoch nicht direkt zur 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 abbilden
Blöcke sind im WordPress-Datenbankdiagramm nicht direkt sichtbar, da sie in wp_posts gespeichert werden (es gibt keine Tabelle wp_blocks), aber sie sind dennoch eine eigenständige Entität.
Daher können wir trotzdem einen Typ Block einführen, um sie abzubilden:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}