🕸 Wie und wo kann GraphQL WordPress verbessern und die REST API ergänzen
Aktualisierung 01/05/2024: Schau dir den Vergleich Gato GraphQL vs WP REST API an.
Letztes Wochenende habe ich den Blogbeitrag 🦸🏿♂️ Gato GraphQL wird jetzt von PHP 8.0 auf 7.1 transpiliert veröffentlicht.
Nachdem ich den Beitrag auf Reddit's /r/php geteilt hatte, startete die Community eine lebhafte Diskussion darüber, wie sinnvoll es ist, GraphQL in WordPress zu verwenden, wie es sich von der WP REST API unterscheidet und ob es gerechtfertigt ist, noch eine weitere API in WordPress einzubringen.
Ich denke, die meisten Kommentare treffen den Nagel auf den Kopf, während anderen einige wichtige Informationen fehlen. GraphQL ist nicht nur eine Schnittstelle, sondern auch eine Implementierung. Das bedeutet, dass verschiedene GraphQL-Server von verschiedenen Anbietern möglicherweise darauf ausgelegt sind, unterschiedliche Eigenschaften zu priorisieren. Daher können wir nicht immer eine einheitliche Erwartung daran haben, was GraphQL bietet, oder ein vollständiges Verständnis davon, wie eine GraphQL-Engine funktioniert.
Zum Beispiel wird die GraphQL-Erfahrung in WordPress und in Laravel unterschiedlich sein, ebenso wie die Erfahrung, die von den verschiedenen Servern WPGraphQL oder Gato GraphQL geboten wird.
Dieser Artikel ist meine Sichtweise auf die Sache und geht auf mehrere Kommentare aus dem Reddit-Beitrag ein.
GraphQL vs WP REST API
[So eine schlechte Idee,] eine GraphQL-API auf WordPress aufzusetzen, das bereits seine eigene REST API verwendet. Nutze einfach die REST API. [Source]
Sowohl die REST API als auch GraphQL verfolgen dasselbe Ziel: der Anwendung die benötigten Daten bereitzustellen. Sie verhalten sich jedoch unterschiedlich darin, wie sie das erreichen: Während REST vordefinierte Endpunkte hat, die einen bestimmten Datensatz liefern, kann GraphQL genau die benötigten Daten liefern.
Dieses unterschiedliche Verhalten kann direkte Auswirkungen auf die Performance der Anwendung haben. Mit REST, wenn wir eine Liste von Beiträgen und einige Daten von jedem Autor des Beitrags abrufen müssen, sind zusätzliche Anfragen erforderlich. Möglicherweise 1 zusätzliche Anfrage für alle Autorendaten oder 1 zusätzliche Anfrage pro Autor. In der Zwischenzeit wartet der Besucher der Website möglicherweise darauf, dass die Seite gerendert wird.
GraphQL verbessert diese Situation, da wir alle Beitrags- und Autorendaten direkt in einer einzigen Anfrage abrufen können und das Rendering der Webseite schneller wird:
{
posts {
id
title
excerpt
date
url
author {
id
name
url
}
}
}Auch wenn wir bereits die REST API in WordPress haben, bedeutet das nicht, dass sie immer das am besten geeignete Werkzeug für jede Aufgabe ist. Klar, wir können sie immer verwenden, aber wenn wir auch Zugang zu GraphQL haben, können wir entscheiden, diese API zu nutzen, wann immer sie einen Vorteil gegenüber REST bietet, und wir fahren damit besser.
Schwierige Erstkonfiguration für GraphQL + Resolver schreiben müssen
Es gibt definitiv ein Argument dafür, dass die Erstkonfiguration für GraphQL exponentiell höher ist als für REST; du hast Recht, dass die Verknüpfungen eingerichtet werden müssen. [Source]
Und...
Was du und fast alle anderen im Web weglassen: Damit dieses API-Format funktioniert, muss man den Parser (Resolver + Typen) schreiben, was eine Reihe von Problemen mit sich bringt, die bei REST nicht vorhanden sind. [Source]
Diese Kommentare sind nicht vollständig zutreffend, denn sowohl WPGraphQL als auch Gato GraphQL haben das WordPress-Datenmodell bereits in das GraphQL-Schema abgebildet (WPGraphQL vollständig, mein Plugin größtenteils).
Nachdem du eines dieser Plugins installiert hast, kannst du sofort damit beginnen, Daten für deine Anwendung abzurufen, ohne Resolver erstellen oder Verknüpfungen zwischen Entitäten einrichten zu müssen.
Es stimmt, dass für den Abruf benutzerdefinierter Daten aus den eigenen Entitäten der Anwendung (wie aus CPTs) diese über Resolver abgebildet werden müssen, und du das tun musst. Aber das ist kein Unterschied zu REST: Wenn du benutzerdefinierte Daten aus deinem CPT benötigst, musst du einen REST-Endpunkt erstellen, um diese benutzerdefinierten Daten abzurufen. Ein benutzerdefinierter Endpunkt ist ebenfalls ein Resolver.
Hinsichtlich der Notwendigkeit von Resolvern sind REST und die GraphQL-API also ziemlich gleichwertig.
Nun, beim Durchsuchen von Websites und Dokumentationen entsteht der Eindruck, dass GraphQL mehr Aufwand für die Einrichtung erfordert. Es ist also etwas Wahres an dieser Annahme dran.
Ich glaube, es gibt dafür einige Gründe. Zum einen umfasst GraphQL (mindestens) zwei Teile:
- das Konzept, was es ist und wie es funktioniert
- die Server, die eine konkrete Implementierung bereitstellen
Wenn man die Dokumentation von GraphQL durchsucht, wie die offizielle Seite graphql.org, konzentriert sie sich auf die Konzepte hinter GraphQL und geht detailliert auf Resolver ein, was sie sind und warum sie benötigt werden.
Das ist nützlich, wenn du eine Anwendung von Grund auf neu baust, z.B. mit Laravel und Lighthouse. In diesem Fall musst du deine Resolver schreiben (aber genauso müsstest du auch deine REST-Endpunkte erstellen).
WordPress ist jedoch bereits die Anwendung, und WPGraphQL und Gato GraphQL sind Lösungen. Diese beiden Plugins haben bereits die Resolver für uns erstellt, sodass wir uns nicht darum kümmern müssen (ähnlich wie die WP REST API auch einen anfänglichen Satz von Endpunkten bereitstellt, sodass wir uns nicht darum kümmern müssen).
Darüber hinaus ist GraphQL entwicklerorientierter, und seine Dokumentation spricht Entwickler direkt an. Entwickler erstellen die Resolver auf der Serverseite, und Entwickler nutzen diese Resolver mit benutzerdefinierten queries auf der Clientseite. Da das Erstellen von Resolvern eine Aufgabe für Entwickler ist, taucht es natürlich und häufig auf.
Für REST ist die Erwartung (glaube ich), dass der Endpunkt, der die benötigten Daten liefert, bereits existiert (wie von der WP REST API bereitgestellt). Wenn nicht, erst dann müssen wir uns um das Einrichten eines benutzerdefinierten Endpunkts kümmern. Daher liegt bei REST weniger Betonung auf dem Erstellen von Resolvern.
Letztendlich liefern sowohl REST als auch GraphQL die benötigten Daten. Aber während REST einen statischen Ansatz fördert, bei dem Endpunkte bereits vorhanden sein sollten und wir uns erst kümmern, wenn sie es nicht sind, fördert GraphQL einen dynamischen Ansatz, bei dem jede query maßgeschneidert ist und wir dann den perfekten Resolver dafür schreiben können.
Am Ende gibt es also keine grundlegenden Unterschiede zwischen REST und GraphQL, nur unterschiedliche Interpretationen, wie sie ihre Anforderungen erfüllen müssen.
Sicherheitslücken + Sicherheitsüberlegungen bei GraphQL
Wir werden eines Tages eine riesige Sicherheitslücke in GraphQL sehen, weil das Schreiben sicherer Interpreter wirklich schwer ist. [Source]
Und...
WordPress ist bereits so massiv, dass es schon ein riesiges Ziel auf dem Rücken hat; das Hinzufügen von IRGENDWELCHEN Plugins birgt viel Risiko, und ein Plugin, das anbietet, buchstäblich alles von WordPress offenzulegen, einschließlich vieler Code-Beispiele zum Umgehen des Sicherheitsmodells, ist ein klares Nein für mich. Nicht-theme-gesteuerter Output sollte so eingeschränkt wie möglich sein (nicht existent, sofern ich ihn nicht anfordere) über das hinaus, was absolut notwendig ist zu exponieren. Ich hoffe, das findet nie Eingang in den Core. [Source]
GraphQL birgt tatsächlich zusätzliche Sicherheitsrisiken, mit denen wir umgehen müssen. Ich stimme diesem Gefühl vollständig zu.
Aber ich glaube nicht, dass es ein so blockierendes Problem ist, dass es eine potenzielle Aufnahme von GraphQL in den WP-Core verhindern würde. Außerdem glaube ich nicht einmal, dass es wirklich schwer zu lösen ist.
Was benötigt wird, ist, dass der GraphQL-Server die vorhandenen Sicherheitsmechanismen von WordPress nutzt, und dass der Entwickler diese Mechanismen verwendet, um sicherzustellen, dass auf ein bestimmtes Feld nur von den entsprechenden Benutzern zugegriffen werden kann:
- Ist der Benutzer eingeloggt?
- Ist der Benutzer der Administrator?
- Hat der Benutzer eine bestimmte Rolle oder Berechtigung?
- Ist der Benutzer der Autor des Beitrags?
Um diesem Vorschlag gerecht zu werden, bietet Gato GraphQL Zugriffskontrolllisten an, damit wir per Konfiguration festlegen können, wer auf welches Feld und welche Direktive zugreifen darf.
Manchmal reicht allein eine ACL nicht aus, und der GraphQL-Server muss zusätzliche Sicherheitsmaßnahmen bereitstellen. Ich werde beschreiben, woran ich gerade für das bevorstehende v0.8 von Gato GraphQL arbeite.
Das Feld posts (zum Abrufen von Beitragsdaten) erfordert keine Autorisierung, jeder Benutzer kann darauf zugreifen, ob eingeloggt oder nicht. Daher werden aus Sicherheitsgründen nur veröffentlichte Beiträge abgerufen.
Es gibt jedoch Situationen, in denen wir auch Entwurfs-/ausstehende/gelöschte Beiträge abrufen müssen, zum Beispiel:
- Zum Erstellen einer statischen Website, die vom Administrator ausgeführt wird, mit Zugriff auf alle Daten der Website
- Für die Autoren des Beitrags, um alle Entwürfe aufzulisten, damit sie diese weiter bearbeiten können
Daraufhin habe ich folgendes Schema entwickelt. Zum Abrufen von Beiträgen wird es 3 Felder geben:
posts: offen für alle, kann nur veröffentlichte Beiträge abrufenmyPosts: offen für alle, ruft nur Beiträge des eingeloggten Benutzers mit jedem Status ab (veröffentlicht/Entwurf/ausstehend/gelöscht)postsForAdmin: nur der Administrator kann darauf zugreifen, ruft beliebige Beiträge mit beliebigem Status ab
Und postsForAdmin ist standardmäßig deaktiviert, erscheint also nicht einmal im GraphQL-Schema, es sei denn, der Administrator aktiviert es explizit (und höchstwahrscheinlich wird es nur zum Erstellen statischer Websites aktiviert).
Eine weitere Situation ist, wenn ein bestimmtes Feld sowohl öffentliche als auch private Daten abrufen kann. Zum Beispiel ruft das Feld option Daten aus der Tabelle wp_options ab. Einige Einträge sind öffentlich (wie blogname), während andere es nicht sind (wie admin_email).
Eine ähnliche Situation besteht beim Abrufen von Meta-Werten über die Felder Post.metaValue, User.metaValue und andere. Zum Beispiel enthält das Benutzer-Meta den Eintrag wp_capabilities, der sicherlich privat ist, während description öffentlich ist. Und dann gibt es last_name, das je nach Anwendung öffentlich oder privat sein kann.
Um den Zugriff auf diese Daten zu sichern, wird das Plugin es ermöglichen, anzugeben, welche Einträge über eine Erlaubnis-/Sperrliste auf der Einstellungsseite abgefragt werden können, und dabei sowohl den vollständigen Eintrag als auch einen Regex akzeptieren:

Das Abfragen der erlaubten Option wird dann funktionieren, während die verweigerte Option einfach null zurückgibt:
{
# This option is allowed
siteName: optionValue(name: "blogname")
# This optionValue is not allowed
adminEmail: optionValue(name: "admin_email")
}Mit angemessenen Sicherheitsmaßnahmen, die vom GraphQL-Server bereitgestellt werden, und gesundem Menschenverstand des Entwicklers sollte die Erstellung einer sicheren GraphQL-API nicht schwierig sein.
GraphQL bringt die Datenbank zum Absturz
GraphQL ist eine reichhaltige Syntax, die tiefe relationale queries ermöglicht. Für ein Ökosystem wie WordPress, in dem die Erweiterbarkeit des Datenmodells aus dem Entity-Attribute-Value-Pattern stammt, führt das zu unglaublichem Verschleiß an einer Datenbank, was dazu führen kann, dass deine Website nicht mehr reagiert, wenn die GraphQL-query tief, kompliziert oder rekursiv ist. WordPress ist bereits dafür bekannt, eine MySQL/MariaDB-Instanz in die Knie zu zwingen, also könnte das Hinzufügen von GraphQL die Sache noch viel schlimmer machen, wenn die queries nicht korrekt geschrieben, authentifiziert und ratenlimitiert sind. [Source]
Die Datenbank zum Absturz zu bringen ist ein ernstes Problem für GraphQL-Server. Ich werde beschreiben, wie Gato GraphQL versucht, dieses Szenario zu vermeiden.
Gato GraphQL verhindert das N+1-Problem bereits durch das architektonische Design. Es schafft das, indem die Engine dafür verantwortlich ist, die Entitäten aus der Datenbank zu laden, nicht der Entwickler.
Beim Auflösen von Verbindungen in einem Resolver ist der zurückgegebene Wert die ID (oder Liste von IDs) der Objekte, nicht das Objekt selbst. Zum Beispiel wird das Abrufen des Autors des Custom Posts so gemacht:
class CustomPostFieldResolver extends AbstractDBDataFieldResolver
{
private CustomPostUserTypeAPIInterface $customPostUserTypeAPI;
public function getClassesToAttachTo(): array
{
return [
CustomPostFieldInterfaceResolver::class,
];
}
public function getSchemaFieldType(string $fieldName): ?string
{
return match($fieldName) {
'author' => SchemaDefinition::TYPE_ID,
default => null,
};
}
public function resolveValue(
TypeResolverInterface $typeResolver,
object $customPost,
string $fieldName,
array $fieldArgs = []
): mixed {
switch ($fieldName) {
case 'author':
return $this->customPostUserTypeAPI->getAuthorID($customPost);
}
return null;
}
public function resolveFieldTypeResolverClass(
TypeResolverInterface $typeResolver,
string $fieldName
): ?string {
switch ($fieldName) {
case 'author':
return UserTypeResolver::class;
}
return null;
}
}Mit der ID der DB-Entität aus resolveValue und dem Typ des Objekts aus resolveFieldTypeResolverClass (dargestellt durch die Klasse UserTypeResolver) kann die GraphQL-Engine dann die Daten für das Objekt laden.
Zum Laden der Daten verwendet die Engine einen sehr effizienten Algorithmus: Er hat Zeitkomplexität O(n), wobei n die Anzahl der Typen in der query ist, nicht die Anzahl der Knoten.
Der Algorithmus erreicht diese Effizienz, weil er keinen Graphen traversiert, sondern die Datenstruktur in einen Stack von Komponenten umwandelt, was viel einfacher aufzulösen ist. (Das "Graph" in GraphQL ist ein Konzept, keine tatsächliche Implementierung.)
Auch wenn die query mehrere Ebenen hat, von denen jede viele Entitäten abruft, kann der Algorithmus das noch gut bewältigen. Zum Beispiel hat das Ausführen der folgenden query, die eine Tiefe von 10 Ebenen hat, keinen großen Einfluss:
{
posts(pagination: { limit: 10 }) {
excerpt
title
url
author {
name
url
posts(pagination: { limit: 10 }) {
title
tags(pagination: { limit: 10 }) {
slug
url
posts(pagination: { limit: 10 }) {
title
comments(pagination: { limit: 10 }) {
content
date
author {
name
posts(pagination: { limit: 10 }) {
title
url
comments(pagination: { limit: 10 }) {
content
date
author {
name
username
url
}
}
}
}
}
}
}
}
}
}
}Die Ausnahme von dieser Effizienz ist beim Abrufen von Meta-Werten über Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue und PostCategory.metaValue (sowie deren Feld metaValues). Das liegt daran, dass die WordPress-Funktionen (get_post_meta, get_user_meta usw.) Daten für jeweils 1 ID abrufen, was bedeutet, dass jede Entität einen Datenbankaufruf erfordert, um ihren Meta-Wert abzurufen. Infolgedessen skaliert das Auflösen von Meta-Werten basierend auf der Anzahl der Knoten, nicht der Anzahl der Typen (der Kommentar des OP trifft in dieser Hinsicht den Nagel auf den Kopf).
Um zu verhindern, dass böswillige Akteure die Meta-Felder nutzen und missbrauchen, wird Gato GraphQL (in v0.8) diese Felder standardmäßig deaktiviert ausliefern. Dann muss der Administrator sie explizit aktivieren und kann dabei diese Felder unter eine Zugriffskontrollliste stellen, sodass die DB zu keinem Zeitpunkt einem Angriffsrisiko ausgesetzt ist.
Rate-Limiting ist ebenfalls eine großartige Idee, ich plane, es für eine kommende Version zu unterstützen.
Und dann gibt es noch die Analyse und das Auferlegen von Beschränkungen hinsichtlich der Komplexität der query (z.B. wie viele Ebenen tief sie ist). Der GraphQL-Server löst die query mit Zeitkomplexität O(n) auf, sodass bezüglich Schleifen nicht viel Schaden angerichtet werden kann. Jedoch könnte eine einzelne query immer noch unbegrenzte Mengen an Daten aus der DB abrufen, und das ist etwas, das wir möglicherweise vermeiden möchten.
Zum Beispiel wird diese einfache query eine riesige Datenmenge in einer einzigen Anfrage liefern (meine Demo-Website hat kaum ein paar Hundert Einträge, sodass ich es mir leisten kann, die Ausführung der query zu demonstrieren):
{
posts000: posts(pagination: { limit: 100 }) {
...PostFields
}
posts100: posts(pagination: { limit: 100, offset: 100 }) {
...PostFields
}
posts200: posts(pagination: { limit: 100, offset: 200 }) {
...PostFields
}
posts300: posts(pagination: { limit: 100, offset: 300 }) {
...PostFields
}
posts400: posts(pagination: { limit: 100, offset: 400 }) {
...PostFields
}
posts500: posts(pagination: { limit: 100, offset: 500 }) {
...PostFields
}
posts600: posts(pagination: { limit: 100, offset: 600 }) {
...PostFields
}
posts700: posts(pagination: { limit: 100, offset: 700 }) {
...PostFields
}
posts800: posts(pagination: { limit: 100, offset: 800 }) {
...PostFields
}
posts900: posts(pagination: { limit: 100, offset: 900 }) {
...PostFields
}
}
fragment PostFields on Post {
id
title
content
date
}Wie man sehen kann, muss die query nicht einmal verschachtelt sein, um Probleme zu verursachen. Das Analysieren der Komplexität einer query ist also eine knifflige Angelegenheit, die eine Feinabstimmung erfordert, um nützlich zu sein.
Ich hoffe, auch die Query-Analyse zu unterstützen, aber es steht nicht auf meiner Liste der hohen Prioritäten, denn mit einer Kombination der anderen Funktionen (wie persisted queries oder custom endpoints in Verbindung mit Zugriffskontrolllisten) können wir böswillige Akteure bereits fernhalten, und wir selbst werden unseren eigenen GraphQL-Dienst nicht missbrauchen (sollten wir nicht!).