Konzepte, Ideen, Strategien
Konzepte, Ideen, StrategienDas GraphQL-Schema für deine WordPress-Site, dein Theme oder Plugin abbilden

Das GraphQL-Schema für deine WordPress-Site, dein Theme oder Plugin abbilden

Du hast also entschieden, GraphQL für deine bestehende WordPress-Site einzusetzen. Großartig! Egal ob für neue oder bestehende Funktionalität – GraphQL muss mit der zugrunde liegenden Datenschicht interagieren. Dafür musst du das Datenmodell deiner Anwendung (ob benutzerdefinierter PHP-Code in deiner WordPress-Site, deinem Theme oder deinem Plugin) in das GraphQL-Schema abbilden.

Wie soll das Mapping durchgeführt werden? Muss es auf einmal erfolgen? Soll es eine exakte Kopie des bestehenden Datenmodells sein? Was ist mit der Korrektur eines unpassenden Namens dabei? Und beim Thema technische Schulden – sollen sie beibehalten oder angegangen werden?

Lass uns einige Strategien erkunden, um das Datenmodell einer bestehenden WordPress-Anwendung in ein GraphQL-Schema abzubilden.

Das Schema in deinem eigenen Tempo abbilden

GraphQL zu einer Anwendung hinzuzufügen ist keine Alles-oder-Nichts-Entscheidung. Dieselbe Anwendung kann gleichzeitig von mehreren APIs betrieben werden, wobei GraphQL so lange neben anderen APIs existiert, wie es nötig ist. Zum Beispiel können wir die bestehende Funktionalität weiterhin mit REST betreiben und GraphQL nur für alle neuen Funktionen einsetzen.

Wenn du eine vollständige Migration zu GraphQL durchführen möchtest, muss das nicht auf einmal geschehen. Die bestehende Funktionalität kann langsam aber stetig zu GraphQL migriert werden, bis GraphQL eines Tages die einzige API der Anwendung ist.

Daher musst du, auch wenn du das vollständige GraphQL-Schema bereits am ersten Tag erstellen könntest, das nicht tun: Zu jedem Zeitpunkt müssen nur die von der Funktionalität benötigten Entitäten im Schema vorhanden sein (über ihre Typen, Felder und Interfaces). Du kannst sie nach und nach, schrittweise, abbilden.

Das Interface nicht mit der Last der Implementierung belasten

Der GraphQL-Server implementiert die Logik für den Zugriff auf die Daten der Anwendung. Er tut dies, indem er WordPress-Funktionen aufruft, wie z. B. get_posts, um Post-Daten abzurufen. Auf dieser Ebene gibt es PHP-Code, der die Resolver bedient.

Ein GraphQL-Schema ist jedoch ein Interface: Es deklariert die Verträge für den Datenzugriff in der API. Es kümmert sich nicht um Implementierungsdetails: Es weiß nichts über WordPress, die Funktion get_posts, die Datenbanktabelle wp_posts oder SQL-Abfragen.

Daher sollten wir so weit wie möglich vermeiden, Informationen zwischen den Schichten durchsickern zu lassen.

Das ist wichtig, weil das Datenmodell häufig durch seine Implementierung belastet wird. WordPress liefert dafür ein klares Beispiel mit dem CPT "attachment", das zur Darstellung von Mediendateien wie Bildern dient.

Da es ein Custom Post Type ist, wird ein Bild wie ein Post behandelt. Dann könnten wir versucht sein, Mediendateien mit dem Typ Post darzustellen, der diese Felder enthält:

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

Aber das ist für die Anwendung möglicherweise nicht angemessen. Die Bedeutung des Feldes "content" ist für einen Post klar, aber nicht für ein Bild. Höchstwahrscheinlich sollte es dort nicht stehen.

Ein Bild wurde in WordPress als CPT modelliert, weil es praktisch war – so konnte bestehende Logik wiederverwendet und in der vorhandenen Tabelle wp_posts gespeichert werden.

Praktisch bedeutet jedoch nicht korrekt, und kann letztendlich zu technischen Schulden führen (d. h. mangelhaftem Code, der nicht behoben werden kann, ohne einen Breaking Change zu verursachen, und daher länger als nötig in der Anwendung verbleibt).

So weit wie möglich wollen wir keine technischen Schulden in unserer Anwendung behalten. Wann immer sich die Gelegenheit bietet, sollten wir sie beheben. Das Abbilden des Datenmodells auf das GraphQL-Schema bietet genau eine solche Gelegenheit, indem wir das Problem auf der Ebene der Datenschnittstelle beheben können.

(Die technischen Schulden bestehen auf Anwendungsebene jedoch weiterhin, sodass wir das Problem nicht vollständig lösen, sondern im Rahmen unserer Möglichkeiten abschwächen.)

Setzen wir diese Idee in die Praxis um. Anstatt einen Typ Post zur Darstellung von Mediendateien zu verwenden, macht es mehr Sinn, einen Typ Media zu haben, der nur die Eigenschaften enthält, die für eine Bild-Entität sinnvoll sind:

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

Im Hintergrund, auf der Implementierungsebene, wird der Field Resolver weiterhin die Funktion get_posts ausführen, um Einträge vom Typ Media aufzulösen – aber das ist keine Angelegenheit des GraphQL-Schemas.

Das GraphQL-Schema vom Datenbankdiagramm entkoppeln

WordPress ist auf Basis von diesem Datenbank-Entity-Relationship-Diagramm implementiert:

Datenbank-Entity-Relationship-Diagramm in WordPress

Das GraphQL-Schema muss auf dem Datenbankdiagramm basieren, aber wir sollten nicht versuchen, eine 1:1-Kopie zu erstellen. Das liegt daran, dass sowohl das GraphQL-Schema als auch das Datenbankdiagramm mit bestimmten Vorbedingungen oder Einschränkungen erstellt wurden, die für das jeweils andere nicht gelten.

Der vorherige Abschnitt zeigt ein Beispiel dafür: Tabelle wp_posts speichert die Daten für den Bild-CPT, aber in GraphQL gibt es zwei verschiedene Typen: Post und Media.

Betrachten wir ein weiteres Beispiel: Kategorien. In WordPress kann ein Post eine Kategorie (oder mehrere) haben, und jeder CPT kann auch seine eigene Kategorie erstellen. Zum Beispiel hat ein CPT namens "event" eine "event_category".

Sowohl Post-Kategorien als auch Event-Kategorien werden in der Tabelle wp_terms gespeichert. Das erleichtert WordPress das Abrufen von Zeilen des einen oder anderen Kategorietyps bei der Ausführung der SQL-Abfrage.

Daher könnten wir versucht sein, Kategorien über den Typ Category abzubilden, auf den sowohl Posts als auch Events verweisen:

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

Allerdings enthält ein Post immer Post-Kategorien, und ein Event enthält immer Event-Kategorien. Die Daten dieser beiden Kategorietypen können in derselben Datenbanktabelle gespeichert sein, werden aber auf der Anwendungsebene nicht vermischt. Post-Kategorie und Event-Kategorie sind zwei verschiedene Entitäten.

GraphQL besitzt ein statisches Typsystem. Um GraphQL optimal zu nutzen, müssen verschiedene Entitäten auf Anwendungsebene durch verschiedene Typen im GraphQL-Schema modelliert werden.

In diesem Fall sollten wir beim Abbilden von Kategorien in das GraphQL-Schema für jede einen anderen Typ erstellen: PostCategory und EventCategory. Dann verweist der Typ Post nur auf PostCategory, und der Typ Event verweist nur auf EventCategory:

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

Wenn wir trotzdem eine Entität im Schema haben möchten, die alle Kategorien umfasst, kann das über ein Interface Category erreicht werden:

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

Auf diese Weise erhalten die Nutzer, die auf die API zugreifen, ein klares Verständnis darüber, welche Daten abgerufen werden – unabhängig davon, wie sie im Datenbankdiagramm abgebildet und in der Datenbank gespeichert sind.

Wenn wir das endgültige GraphQL-Schema haben, können wir feststellen, dass seine Form dem WordPress-Datenbankdiagramm zwar ähnelt, sich aber deutlich davon unterscheidet:

GraphQL-Schema

Die Benennung von Feldern anpassen und dabei dem statischen Typsystem folgen

Felder sollten, so weit wie möglich, dieselbe Benennung beibehalten, die sie in der Anwendung haben.

Zum Beispiel können wir einen Post mit der Funktion wp_insert_post erstellen, und der Post hat die Eigenschaften "title" und "content". Diese Namen eignen sich auch für das GraphQL-Schema (auch wenn sie leichte Anpassungen benötigen), also sollten wir sie beibehalten:

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

Das ist aber nicht immer der Fall. Wie wir zuvor gesehen haben, müssen Custom Posts in ihre eigenen Entitäten entkoppelt werden. Während die Funktion get_posts eine Liste aller CPTs abruft, ruft ein äquivalentes Feld posts im Root-Typ des Schemas nur Entitäten vom Typ Post ab, aber nicht Page (das ebenfalls ein CPT ist):

type QueryRoot {
  posts: [Post]!
}

Wie erhalten wir dann die Liste aller Posts und Seiten? Über ein weiteres Feld, customPosts, das die Entitäten jedes CPTs abruft, der unter dem Union-Typ CustomPostUnion abgebildet ist:

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

Die wichtigste Lektion ist diese: Die Benennung, die wir für das GraphQL-Schema wählen, muss an den Typ der abgerufenen Entität angepasst werden. Und aufgrund von GraphQLs starker Typisierung kann dieser Typ auf Anwendungs- und API-Ebene unterschiedlich sein.

In diesem Fall kann ein "post" in WordPress jeden "custom post type" bedeuten, in GraphQL ist ein "post" aber zwingend ein Post. Wenn ein Feld Custom Posts abruft, muss das Feld im GraphQL-Schema customPosts heißen und nicht posts. Ebenso muss ein Input, der eine ID für einen Custom Post erhält, customPostID und nicht postID heißen.

Mapping für das Feld customPosts

Diese Lektion gilt zum Beispiel für Kommentare. Ein Kommentar kann zu jedem CPT hinzugefügt werden, nicht nur zu Posts. Daher muss der Typ Comment das deutlich machen, indem er das Feld customPost enthält (und nicht post):

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

Vordefinierte String-Werte in Enums umwandeln, wenn möglich in Großbuchstaben

Enumerationstypen werden per Konvention in Großbuchstaben definiert. Zum Beispiel liefert die Dokumentation von graphql.org folgendes Beispiel:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Wann immer wir einen neuen Enum-Typ erstellen müssen, sollten wir Großbuchstaben für seine definierten Konstanten verwenden. Beim Migrieren des Datenmodells aus der Anwendung können wir jedoch auf bestimmte Mengen vordefinierter Werte stoßen, die wir als Enum abbilden können, deren Werte aber Kleinbuchstaben-Strings sind.

Als Beispiel: Posts in WordPress haben eine Eigenschaft "status", die einen der folgenden Werte enthält:

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

Beim Abbilden dieser Eigenschaft im Schema könnte das Feld Post.status einen String zurückgeben, wie hier:

type Post {
  status: String!
}

Da der Status jedoch zwingend einer dieser vordefinierten Werte sein wird und kein anderer, bilden wir ihn lieber als Enum ab:

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

Jetzt könnten wir ein Problem haben: Der Enum PUBLISH wird in der Anwendung in den String-Wert "PUBLISH" umgewandelt, nicht in "publish".

Durch Verwendung eines Großbuchstaben-Werts anstelle des erwarteten Kleinbuchstaben-Werts kann die Logik in der Anwendung gestört werden. Tatsächlich funktioniert die Ausführung des folgenden Codes in WordPress nicht:

// This will retrieve all posts, not only the published ones
$published_posts = get_posts([
  "post_status" => "PUBLISH",
]);

In diesem Fall können wir erwägen, Konvention gegen Bequemlichkeit einzutauschen – wir verwenden trotzdem ein Enum für die Abbildung der Konstanten, aber in Kleinbuchstaben:

enum Status {
  publish
  draft
  pending
  trash
}

Mit anderen Worten: Wir können einen Mittelweg zwischen Korrektheit und Pragmatismus finden. Wir sollten Best Practices beim Aufbau des GraphQL-Schemas verwenden, uns aber erlauben, davon abzuweichen, wenn es sinnvoll ist.