Blog

🤔 Warum hat das neue Gato GraphQL 1,5 Jahre bis zur Veröffentlichung gebraucht?

Leonardo Losoviz
Von Leonardo Losoviz ·

Version 0.9 von Gato GraphQL wurde gerade veröffentlicht. Fast 1,5 Jahre Entwicklungsarbeit und über 16000 Commits waren nötig, um sie fertigzustellen. Das ist wirklich eine lange Zeit!

Als ich die Ankündigung auf Hacker News teilte, erhielt ich folgende Frage:

[...] Ich bin neugierig, was 16k Commits erfordert hat. Die Projekte, an denen ich beteiligt war und die mehr als zehntausend Commits hatten, wurden von vielen Dutzenden oder Hunderten von Vollzeitkräften entwickelt. [...] Gibt es eine Komplexität, die überwunden werden musste, auf die der Beitrag nicht eingeht?

Die Commit-Anzahl ist keine sehr zuverlässige Kennzahl, da ich eine sehr einfache Änderung vornehmen und diese als einzelnen Commit pushen kann. Viele dieser 16k Commits waren "typo"-Commits oder haben lediglich eine Beschreibung in einer README verbessert.

Dennoch gibt die Commit-Anzahl eine Vorstellung vom tatsächlichen Aufwand. Es gab auch viele Commits mit zahlreichen Änderungen, darunter Dutzende und sogar Hunderte von Änderungen auf einmal. Die Unterschiede zwischen den Versionen 0.8 und 0.9 sind tatsächlich enorm, und das erforderte Aufwand und Zeit.

In diesem Blogbeitrag beschreibe ich, welche Änderungen das sind, um zu erklären, warum es so lange gedauert hat. Dabei gebe ich auch einen Vorgeschmack auf einige erweiterte Funktionen, die der Codebasis hinzugefügt wurden und mit der kommenden Version 1.0 das Licht der Welt erblicken werden.

Hintergrund des GraphQL-Servers

Zunächst teile ich ein bisschen die Geschichte der Engine und technische Details darüber, wie sie funktioniert.

(Dies ist hauptsächlich für Entwickler relevant; wenn du dich nicht für technische Details interessierst, kannst du gerne zum nächsten Abschnitt springen.)

Gato GraphQL basiert auf PoP, einer Engine, die Komponenten in PHP rendert (ähnlich wie React oder Vue in JavaScript). Die Abhängigkeit von dieser Engine ist absolut, weshalb das Plugin im Monorepo GatoGraphQL/GatoGraphQL auf GitHub gehostet wird.

Hinter den Kulissen sieht diese Abhängigkeit so aus:

Gato GraphQL löst eine GraphQL-Query auf, indem es sie zunächst in ein gleichwertiges Komponentenmodell umwandelt, das PoP dann auflöst, indem es alle erforderlichen Daten abruft, und diese Daten dann die Form der GraphQL-Query erhalten.

Als ich irgendwann um 2013/2014 anfing, an PoP zu arbeiten, gab es kein GraphQL, und die Methodik zur Auflösung eines Komponentenmodells in Daten wurde von Grund auf entworfen und implementiert. Das Fehlen eines Modells, dem man folgen konnte (wie GraphQL für Konzepte und das graphql-js-Referenzprojekt für eine Implementierung), war sowohl ein Hindernis als auch ein Segen, wie ich später erklären werde.

PoP wurde ursprünglich so konzipiert, dass es die gesamte Website als HTML serverseitig rendert, während es die Rohdaten im JSON-Format bereitstellt, wenn man ?output=json an die URL der Seite anhängt, und weitere URL-Parameter die Auswahl der abzurufenden Daten (Einstellungen, DB-Objektdaten) ermöglichen.

Bitte klicke auf die folgenden Links (alle verweisen auf dieselbe Webseite, nur mit unterschiedlichen URL-Parametern) und beachte, wie sie sich unterscheiden:

Wenn du auf den letzten Link klickst, kommt eine Erkenntnis: Das ist praktisch GraphQL! Der einzige große Unterschied ist, dass die Daten in der Antwort implizit sind, da sie bereits durch die Komponenten (in PHP) definiert wurden, die in die Seite eingebunden wurden. GraphQL hingegen ermöglicht es uns, über eine Query zu entscheiden, welche Daten abgerufen werden sollen.

Als ich also irgendwann um 2019 von GraphQL erfuhr, war es für mich selbstverständlich, PoP auch als GraphQL-Server einzusetzen. Alles, was es tun musste, war, die GraphQL-Query als Input zu akzeptieren und on-the-fly ein Komponentenmodell basierend auf der Query zu erstellen.

Und genau das habe ich getan. Und es hat gut funktioniert. Aber es war langsam, weil PoP sein eigenes Eingabeformat verstand, sodass die GraphQL-Query an das PoP-Format angepasst werden musste:

  1. Die GraphQL-Query parsen; dann
  2. Die Query in das PoP-Format umwandeln; dann
  3. Das PoP-Format parsen

Das Parsen der GraphQL-Query wurde dann zweimal durchgeführt (einmal für GraphQL, einmal für PoP), und das PoP-Format wurde nicht über einen AST aufgelöst, sondern indem die Query-Zeichenkette immer wieder geparst wurde. (Keinen AST zu verwenden war schlechtes Coding, aber ich hatte keine Spezifikation, der ich folgen konnte, und die Entwicklung verlief organisch, wo ein einfaches substr(...) jeden Tag die Situation rettete.)

Deshalb sage ich, dass das Fehlen der GraphQL-Spezifikation ein Hindernis war, da meine Lösung langsam war (und das war die Situation bei Version 0.8). Also beschloss ich, das zu beheben.

Die Engine in GraphQL-first umwandeln

Die Lösung, für die ich mich entschied, ist, PoP nativ die GraphQL-Sprache sprechen zu lassen. Dann würde das Übergeben einer GraphQL-Query an PoP als Input bereits in das Komponentenmodell umgewandelt, ohne dass ein zusätzlicher Adapter benötigt wird oder Dinge zweimal gemacht werden müssen.

Das bedeutete, dass das PoP-Projekt neu ausgerichtet werden musste: von einer PHP-Bibliothek, die Komponenten für Websites serverseitig rendert und zur Auflösung von GraphQL-Queries angepasst wurde, hin zu einem echten GraphQL-Server.

Die Codebasis unterzog sich dann einer massiven Transformation und führte den GraphQL-AST als Grundlage ein, um den Zustand zwischen allen PHP-Diensten in der Engine zu kommunizieren. GraphQL-AST-Objekte sind jetzt die Inputs für PoP (anstelle von Query-Zeichenketten).

Andere GraphQL-Server in PHP stützen sich auf graphql-php, aber das Plugin Gato GraphQL nicht. Das ist eine schlechte Nachricht hinsichtlich des Wartungsaufwands (da ich nicht wiederverwenden kann, was jemand anderes kodiert hat), aber eine gute Nachricht hinsichtlich der Unabhängigkeit: Ich kann entscheiden, meinem Plugin in meinem eigenen Tempo und nach meinen eigenen Kriterien benutzerdefinierte Funktionen hinzuzufügen (weshalb das Plugin bereits das „oneof"-Input-Objekt bereitstellt).

Und wie der folgende Abschnitt zeigen wird, ist das ein großer Vorteil.

Originelle Funktionen in GraphQL einbauen

GraphQL wird normalerweise mit dem Datenabruf assoziiert. Natürlich kannst du beliebige Daten (Beiträge, Benutzer, Kommentare usw.) von Gato GraphQL abrufen:

query {
  posts(
    pagination: { limit: 5, offset: 20 }
    sort: { by: DATE, order: ASC }
  ) {
    id
    title
    content
    url
    author {
      id
      name
      url
    }
    comments {
      id
      date
      content
    }
  }
}

Aber das ist nur die einfache Seite. GraphQL kann auch für viele andere Anwendungsfälle genutzt werden, darunter Datenmanipulation und -transformation sowie sogar der Einsatz von GraphQL in einer Pipeline als Vermittler zwischen Diensten.

Einige Beispiele, bei denen GraphQL nützlich ist:

  • Informationen aus einer oder mehreren Quellen extrahieren (z. B. Benutzer von WordPress-Websites und Newsletter-Kontaktdaten von Mailchimp), die Daten kombinieren und sie gemeinsam als einen einzigen Datensatz analysieren
  • Operationen durchführen, um den Inhalt der Website anzupassen:
    • Einmalig, z. B. beim Migrieren einer Website auf eine andere Domain und dem Ersetzen von "www.myoldsite.com" durch "mynewsite.com" überall im Inhalt und in den Metadaten
    • Fortlaufend, z. B. um jedes "http://" durch "https://" zu ersetzen, wenn ein Autor einen neuen Blogbeitrag veröffentlicht
  • Die Google Translate API nutzen, um alle Blogbeiträge in eine andere Sprache zu übersetzen
  • Automatisch einen Tweet senden, nachdem ein Blogbeitrag veröffentlicht wurde

PoP war so konzipiert worden, diese anderen Anwendungsfälle zu unterstützen, über Funktionen, die von GraphQL (von Natur aus) nicht unterstützt werden, wie z. B.:

  • Unterstützung von „Funktionalitäts"-Feldern (zusätzlich zu „Daten"-Feldern), die allen Typen im Schema hinzugefügt werden
  • Das Ergebnis eines Feldes als Input an ein anderes Feld übergeben, innerhalb derselben Query
  • Direktiven zusammensetzen, sodass eine Direktive das Verhalten einer anderen Direktive ändern kann
  • Dynamisch entscheiden, ob eine Direktive angewendet wird oder nicht, basierend auf dem Wert des Feldes

Und ich wollte diese Funktionen auf keinen Fall aus dem GraphQL-Server entfernen: Ich hatte sie bereits kodiert, und sie sind zweifellos wertvoll.

Der zweite Grund, warum v0.9 so lange gedauert hat, ist also, dass ich auch einen Weg finden musste, diese neuartigen Fähigkeiten in GraphQL zu integrieren, ohne die GraphQL-Spezifikation zu verletzen (das Einführen neuer Elemente in die GraphQL-Syntax war zum Beispiel keine Option).

Ein Beispiel für Datenmanipulation in GraphQL

Die neuartigen Fähigkeiten, die das Plugin in GraphQL eingeführt hat, werden in naher Zukunft mit dem Release von Version 1.0 sichtbarer werden. Aber du kannst schon jetzt einen Vorgeschmack auf einige davon bekommen.

Die folgende GraphQL-Query ruft eine Liste von Benutzereinträgen von einer externen REST-API ab (die mit @remove aus der Antwort entfernt werden kann); gibt diese Daten in ein anderes Feld innerhalb derselben Query ein; extrahiert die E-Mail-Eigenschaft aus jedem Eintrag; und wandelt die E-Mail schließlich in Großbuchstaben um, aber nur wenn die Sprache dieses Eintrags Englisch oder Deutsch ist:

###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes
{
  # Retrieve data from a REST API endpoint
  userEntries: _sendJSONObjectCollectionHTTPRequest(
    input: {
      url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
    }
  ) # @remove   # <= Uncomment this directive to not print the API data
 
  emails: _echo(value: $__userEntries)
 
    # Iterate all the entries, passing every entry
    # (under the dynamic variable $userEntry)
    # to each of the next 4 directives
    @underEachArrayItem(
      passValueOnwardsAs: "userEntry"
      affectDirectivesUnderPos: [1, 2, 3, 4]
    )
 
      # Extract property "lang" from the entry
      # via the functionality field `_objectProperty`,
      # and pass it onwards as dynamic variable $userLang
      @applyField(
        name: "_objectProperty"
        arguments: {
          object: $userEntry,
          by: {
            key: "lang"
          }
        }
        passOnwardsAs: "userLang"
      )
 
      # Execute functionality field `_inArray` to find out
      # if $userLang is either "en" or "de", and place the
      # result under dynamic variable $isSpecialLang
      @applyField(
        name: "_inArray"
        arguments: {
          value: $userLang,
          array: ["en", "de"]
        }
        passOnwardsAs: "isSpecialLang"
      )
 
      # Extract property "email" from the entry
      # and set it back as the value for that entry
      @applyField(
        name: "_objectProperty"
        arguments: {
          object: $userEntry,
          by: {
            key: "email"
          }
        }
        setResultInResponse: true
      )
 
      # If $isSpecialLang is `true` then execute
      # directive `@strUpperCase` 
      @if(condition: $isSpecialLang)
        @strUpperCase
}

Das ist die Antwort (beachte, wie nur bestimmte E-Mails in Großbuchstaben umgewandelt wurden):

{
  "data": {
    "userEntries": [
      {
        "email": "abracadabra@ganga.com",
        "lang": "de"
      },
      {
        "email": "longon@caramanon.com",
        "lang": "es"
      },
      {
        "email": "rancotanto@parabara.com",
        "lang": "en"
      },
      {
        "email": "quezarapadon@quebrulacha.net",
        "lang": "fr"
      },
      {
        "email": "test@test.com",
        "lang": "de"
      },
      {
        "email": "emilanga@pedrola.com",
        "lang": "fr"
      }
    ],
    "emails": [
      "ABRACADABRA@GANGA.COM",
      "longon@caramanon.com",
      "RANCOTANTO@PARABARA.COM",
      "quezarapadon@quebrulacha.net",
      "TEST@TEST.COM",
      "emilanga@pedrola.com"
    ]
  }
}

Probiere es selbst aus! Drücke den „Run"-Button, um die Query auszuführen:

###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes {
  # Retrieve data from a REST API endpoint
  userEntries: _sendJSONObjectCollectionHTTPRequest(
    input: {
      url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
    }
  )
  # @remove   # <= Uncomment this directive to not print the API data
  emails: _echo(value: $__userEntries)
    # Iterate all the entries, passing every entry
    # (under the dynamic variable $userEntry)
    # to each of the next 4 directives
    @underEachArrayItem(
      passValueOnwardsAs: "userEntry"
      affectDirectivesUnderPos: [1, 2, 3, 4]
    )
      # Extract property "lang" from the entry
      # via the functionality field `_objectProperty`,
      # and pass it onwards as dynamic variable $userLang
      @applyField(
        name: "_objectProperty"
        arguments: { object: $userEntry, by: { key: "lang" } }
        passOnwardsAs: "userLang"
      )
      # Execute functionality field `_inArray` to find out
      # if $userLang is either "en" or "de", and place the
      # result under dynamic variable $isSpecialLang
      @applyField(
        name: "_inArray"
        arguments: { value: $userLang, array: ["en", "de"] }
        passOnwardsAs: "isSpecialLang"
      )
      # Extract property "email" from the entry
      # and set it back as the value for that entry
      @applyField(
        name: "_objectProperty"
        arguments: { object: $userEntry, by: { key: "email" } }
        setResultInResponse: true
      )
      # If $isSpecialLang is `true` then execute
      # directive `@strUpperCase`
      @if(condition: $isSpecialLang)
        @strUpperCase
}

Ich hatte erwähnt, dass das Fehlen von GraphQL als Leitfaden ein Hindernis war, aber (im Nachhinein) auch ein Segen. Das liegt daran, dass ich nicht die Einschränkungen der GraphQL-Spezifikation hatte und mir erlauben konnte, von diesen neuartigen Fähigkeiten zu träumen.

Und jetzt, wo diese Funktionen nach Gato GraphQL migriert wurden, kann es ein unglaublich nützlicher Verbündeter für alles rund um den Abruf, die Manipulation und die Transformation von Inhalten für deine WordPress-Website sein. (Auch wenn sie nur mit der kommenden v1.0 zugänglich sein werden.)

Es hat eine Weile gedauert, aber der Aufwand hat sich definitiv gelohnt.

Probiere es aus!

Bist du davon überzeugt, dass das lange Warten es wert war? Ich hoffe es!

Los geht's, lade das Plugin herunter und schau es dir an:

Möchtest du Neuigkeiten über die Entwicklung, neue Dokumentation und kommende Releases einschließlich v1.0 erhalten? Dann kannst du dich gerne für den Newsletter anmelden.

Möchtest du den Open-Source-Code auf GitHub erkunden? Schau dir GatoGraphQL/GatoGraphQL an (und gib ihm gerne einen Stern... Wir lieben Sterne! ⭐️⭐️⭐️)

Übrigens, welche Inhaltstransformationen musst du in WordPress durchführen (für die du vielleicht bereits ein dediziertes kommerzielles Plugin verwendest)? Bitte schick mir eine Nachricht und schildere deinen Anwendungsfall.

Wenn dir gefällt, was du siehst, teile es mit deinen Freunden und Kollegen und hilf dabei, die Begeisterung zu verbreiten ❤️.


Abonniere unseren Newsletter

Bleib über alle Updates zu Gato GraphQL auf dem Laufenden.