Blog

👨🏻‍💻 GraphQL als (eine Art) Programmiersprache

Leonardo Losoviz
Von Leonardo Losoviz ·

GraphQL, obwohl es die GraphQL-Sprache besitzt, würde man normalerweise nicht als Programmiersprache bezeichnen, da es so viele Dinge gibt, die wir mit Programmiersprachen tun können, die wir mit GraphQL nicht tun können.

GraphQL wird normalerweise verwendet, um Daten abzurufen, zum Beispiel um eine Website auf dem Client zu rendern, und um Daten zu verändern, zum Beispiel um einen Beitrag zu erstellen. Und das war's im Grunde.

(Andere Verwendungen sind einfach Kombinationen dieser beiden vorherigen Fälle. Zum Beispiel kann ein API-Gateway Daten von einem internen Server abrufen/verändern, der nicht dem Client ausgesetzt ist.)

Daten in GraphQL abrufen:

query PrintPostTitle($postID: ID!)
{
  post(by: { id: $postID }) {
    title
  }
}

...hat dieses (mehr oder weniger) Äquivalent in PHP:

function printPostTitle(int $postID)
{
  $post = getPost($postID);
  echo $post->title;
}

(Alle folgenden Beispiele verwenden PHP als Programmiersprache zum Vergleich.)

Daten in GraphQL verändern:

query UpdatePost($postID: ID!, $title: String!)
{
  updatePost(
    by: { id: $postID },
    input: { title: $title }
  ) {
    title
  }
}

...hat dieses (mehr oder weniger) Äquivalent in PHP:

function updatePost(int $postID, string $title)
{
  $post = getPost($postID);
  $post->update(['title' => $title]);
}

Das reicht aus, weil auf GraphQL normalerweise von einem Client aus zugegriffen wird (geschrieben in einer Programmiersprache, wie JavaScript, PHP, Java oder andere), der die Logik enthält, was mit den Daten zu tun ist. GraphQL wird also nicht allein verwendet, sondern als Begleiter von jemand anderem.

Wenn GraphQL aber allein verwendet werden könnte, dann ließen sich viele neue Anwendungsfälle einfach mit GraphQL lösen, was GraphQL ermöglichen würde, in neuartigen Umgebungen eingesetzt zu werden und für zusätzliche Aufgaben im Anwendungs-Stack verantwortlich zu sein.

Damit das passieren kann, muss GraphQL jedoch viele der Funktionen von Programmiersprachen unterstützen.

Die Programmiersprachenfunktionen, die GraphQL unterstützt, sind begrenzt. Zum Beispiel kann die Verwendung der Direktive @include (oder @skip) und das Übergeben einer Variable als Input als (eine Art) bedingte Logik betrachtet werden:

query PrintPostProperties($postID: ID!, $addContent: Boolean!)
{
  post(by: { id: $postID }) {
    title
    content @include(if: $addContent)
  }
}

Diese query hat dieses PHP-Äquivalent:

function printPostProperties(int $postID, bool $addContent)
{
  $post = getPost($postID);
  echo $post->title;
  if ($addContent) {
    echo $post->content;
  }
}

Das war's im Grunde. GraphQL fehlt es an Rekursionen, dynamischen Variablen (deren Werte zur Laufzeit berechnet und der Variable zugewiesen werden, nicht als Input im Dictionary), Variablenzuweisungen (z.B.: einem Feld-Output eine Variable zuweisen, die dann als Argument in ein anderes Feld übergeben werden kann) und anderem.

Überlege, wie du eine Lösung nur mit GraphQL für folgendes Problem implementieren würdest:

  • Einen Webhook erstellen, der von einem Dienst aufgerufen werden soll, wenn sich ein neuer Benutzer bei diesem Dienst anmeldet; der Benutzer hat möglicherweise den Newsletter abonniert (angegeben durch das Feld marketing_optin im Payload des Webhooks); in diesem Fall muss der Webhook die E-Mail-Adresse des Benutzers (im Feld email im Payload des Webhooks) in einer Mailchimp-Liste registrieren.

Hältst du das für machbar? einfach? schwierig? unmöglich?

Bei Gato GraphQL, wollen wir dieses Problem nur mit GraphQL lösen. Und viele weitere Probleme. Deshalb haben wir intensiv darüber nachgedacht, wie wir Eigenschaften von Programmiersprachen unterstützen können.

Lass uns erkunden, welche Programmierfunktionen wir auf unserem GraphQL-Server unterstützt haben. Am Ende dieses Beitrags werden wir sehen, wie wir dieses Problem lösen können.

Funktionalität

Felder in GraphQL liefern normalerweise Daten, wie den Titel, den Inhalt oder Daten eines Beitrags. Aber wir können Felder auch als "Funktionalität" implementieren.

Zum Beispiel die Zeit in PHP ausgeben:

function printTime()
{
  echo time();
}

...kann mit dem Feld _time in GraphQL gemacht werden:

{
  _time
}

Beachte, dass die Funktion time zu keinem Typ gehört, daher gehört auch das Feld _time zu keinem. Als solches ist es ein globales Feld, und es ist unter jedem Typ des GraphQL-Schemas zugänglich:

{
  posts {
    _time
  }
}

Weitere Beispiele für Funktionalitätsfelder sind:

  • _arrayItem
  • _arrayJoin
  • _date
  • _equals
  • _inArray
  • _intAdd
  • _isEmpty
  • _isNull
  • _makeTime
  • _objectProperty
  • _sprintf
  • _strContains
  • _strRegexReplace
  • _strSubstr

Funktionen

Wir können Logikeinheiten in Funktionen aufteilen und eine Funktion eine andere aufrufen lassen:

function printPostProperties(int $postID)
{
  $post = getPost($postID);
  printPostTitle();
  printPostContent();
}
 
function printPostTitle(Post $post)
{
  echo $post->title;
}
 
function printPostContent(Post $post)
{
  echo $post->content;
}

In GraphQL können wir ähnlich die query-Operation (oder mutation) im Dokument in mehrere query-Operationen aufteilen und eine Operation von anderen "abhängig" machen, sodass jene zuerst ausgeführt werden:

query PrintPostTitle($postID: ID!)
{
  postWithTitle: post(by: { id: $postID }) {
    title
  }
}
 
query PrintPostContent($postID: ID!)
{
  postWithContent: post(by: { id: $postID }) {
    content
  }
}
 
query PrintPostProperties
  @depends(on: [
    "PrintPostTitle",
    "PrintPostContent"
  ])
{
  # ...
}

In dieser query wird beim Ausführen der GraphQL-query mit ?operationName=PrintPostProperties am Endpoint zuerst die queries PrintPostTitle und PrintPostContent ausgeführt, und erst dann PrintPostProperties.

Dies ist über Multiple Query Execution möglich.

Dynamische Variablen

Wir können einen Wert berechnen und ihn zur Laufzeit einer Variable zuweisen. Dann können wir basierend auf diesem Wert bedingt eine Funktionalität ausführen oder nicht:

function printPostProperties(int $postID)
{
  $post = getPost($postID);
  echo $post->title;
  
  $addContent = isUserLoggedIn();
  if ($addContent) {
    echo $post->content;
  }
}

In GraphQL können wir einen Wert unter einer dynamischen Variable in einer Operation "exportieren" und diesen Wert dann in einer anderen Operation lesen:

query ExportAddContent
{
  addContent: isUserLoggedIn
    @export(as: "addContent")
}
 
query PrintPostProperties($postID: ID!)
  @depends(on: "ExportAddContent")
{
  post(by: { id: $postID }) {
    title
    content @include(if: $addContent)
  }
}

Beachte, dass die Variable $addContent, die einen zur Laufzeit berechneten Wert enthält, in der Operation PrintPostProperties gelesen, aber nicht deklariert wird, da sie eine dynamische Variable ist.

Funktionen bedingt ausführen

Eine Alternative zum vorherigen Beispiel ist, Logik in Funktionen zu gruppieren und dann eine Funktion basierend auf dem Wert der dynamischen Variable bedingt auszuführen oder nicht:

function printPostProperties(int $postID)
{
  $post = getPost($postID);
  printPostTitle();
  
  $addContent = isUserLoggedIn();
  if ($addContent) {
    printPostContent();
  }
}
 
function printPostTitle(Post $post)
{
  echo $post->title;
}
 
function printPostContent(Post $post)
{
  echo $post->content;
}

In GraphQL können wir die Direktive @include auf die Operation anwenden:

query ExportAddContent
{
  addContent: isUserLoggedIn
    @export(as: "addContent")
}
 
query PrintPostTitle($postID: ID!)
{
  postWithTitle: post(by: { id: $postID }) {
    title
  }
}
 
query PrintPostContent($postID: ID!)
  @depends(on: "ExportAddContent")
  @include(if: $addContent)
{
  postWithContent: post(by: { id: $postID }) {
    content
  }
}
 
query PrintPostProperties
  @depends(on: [
    "PrintPostTitle",
    "PrintPostContent"
  ])
{
  # ...
}

Jetzt wird die Operation PrintPostContent nur ausgeführt, wenn $addContent true ist.

Variablen zuweisen, als Input wiederverwenden

Passen wir das vorherige Beispiel leicht an, in dem die Bedingung "addContent" daran geknüpft war, ob der Benutzer eingeloggt ist oder nicht.

In diesem anderen Beispiel ist "addContent" true, wenn heute Wochenende ist, was einige Logik zum Berechnen beinhaltet:

  • Das heutige Datum abrufen
  • Es in den Tagesnamen formatieren, in Kleinbuchstaben
  • Prüfen, ob es "saturday" oder "sunday" ist

In PHP:

function addContent()
{
  $today = time();
  $dayName = date('l', $today);
  $lcDayName = strtolower($dayName);
  $isWeekend = in_array(
    $lcDayName,
    ['saturday', 'sunday']
  );
  return $isWeekend;
}
 
function printPostProperties(int $postID)
{
  $post = getPost($postID);
  echo $post->title;
 
  $addContent = addContent();
  if ($addContent) {
    echo $post->content;
  }
}

In GraphQL:

query ExportAddContent
{
  today: _time
  dayName: _date(format: "l", timestamp: $__today)
  lcDayName: _strLowerCase(text: $__dayName)
  isWeekend: _inArray(
    value: $__lcDayName
    array: ["saturday", "sunday"],
  )
    @export(as: "addContent")
}
 
query PrintPostProperties($postID: ID!)
  @depends(on: "ExportAddContent")
{
  post(by: { id: $postID }) {
    title
    content @include(if: $addContent)
  }
}

In der Operation ExportAddContent ist der Wert jedes abgefragten Felds sofort für die darunter liegenden Felder verfügbar, unter der dynamischen Variable $__fieldName. Auf diese Weise kann der Output eines Felds sofort als Input eines anderen Felds verwendet werden, bereits innerhalb derselben Operation.

Dies ist dank Field to Input möglich.

Einen Wert dynamisch ändern

In diesem PHP-Beispiel ändern wir den Wert einer Variable, wenn der eingeloggte Benutzer ein Administrator ist, in welchem Fall dem Inhalt des Beitrags ein Link zum Bearbeiten des Beitrags hinzugefügt wird:

function isAdminUser()
{
  $user = getCurrentUser();
  return in_array("administrator", $user->roles);
}
 
function printPostContent(int $postID)
{
  $post = getPost($postID);
  $postContent = $post->content;
 
  $isAdminUser = isAdminUser();
  if ($isAdminUser) {
    $postContent = sprintf(
      '%s<p><a href="%s">%s</a></p>',
      $postContent,
      $post->edit_url,
      '(Admin only) Edit post'
    ) 
  }
 
  echo $postContent;
}

In GraphQL können wir bedingt eine Operation oder eine andere ausführen und dabei unterschiedliche Werte für ein Feld erzeugen:

query InitializeDynamicVariables
{
  isAdminUser: _echo(value: false)
    @export(as: "isAdminUser")
}
 
query ExportConditionalVariables
  @depends(on: "InitializeDynamicVariables")
{
  me {
    roleNames
    isAdminUser: _inArray(
      value: "administrator",
      array: $__roleNames
    )
      @export(as: "isAdminUser")
  }
}
 
query RetrieveContentForAdminUser($postId: ID!)
  @depends(on: "ExportConditionalVariables")
  @include(if: $isAdminUser)
{
  post(by: { id : $postId }) {
    originalContent: content
    wpAdminEditURL
    content: _sprintf(
      string: "%s<p><a href=\"%s\">%s</a></p>",
      values: [
        $__originalContent,
        $__wpAdminEditURL,
        "(Admin only) Edit post"
      ]
    )
  }
}
 
query RetrieveContentForNonAdminUser($postId: ID!)
  @depends(on: "ExportConditionalVariables")
  @skip(if: $isAdminUser)
{
  post(by: { id : $postId }) {
    content
  }
}
 
query ExecuteAll
  @depends(on: [
    "RetrieveContentForAdminUser",
    "RetrieveContentForNonAdminUser"
  ])
{
  # ...
}

Durch die Verwendung der Direktiven @include und @skip mit derselben dynamischen Variable als Input sind die Operationen RetrieveContentForAdminUser und RetrieveContentForNonAdminUser sich gegenseitig ausschließend.

Arrays iterieren

Angenommen, wir möchten die Elemente in einem Array iterieren und diese Werte in Großbuchstaben umwandeln:

function printUserRolesAsUppercase(int $userID)
{
  $user = getUser($userID);
  foreach ($user->roles as $role) {
    echo strtoupper($role);
  }
}

In GraphQL können wir die Direktive @underEachArrayItem über die Array-Elemente iterieren lassen und jeden dieser Werte an die folgende Direktive in der Kette übergeben, in diesem Fall @strUpperCase:

query PrintUserRolesAsUppercase($userID: ID!)
{
  user(by: { id: $userID }) {
    roles
      @underEachArrayItem
        @strUpperCase
  }
}

Dies ist dank zusammensetzbarer Direktiven möglich.

Massen-CRUD-Operationen

CRUD steht für Create (Erstellen), Read (Lesen), Update (Aktualisieren) und Delete (Löschen) – das sind die Operationen, die wir auf Ressourcen (Beiträge, Benutzer, usw.) anwenden.

Bulk-Lesen in PHP sieht so aus:

function getPostTitles()
{
  $posts = getPosts();
  foreach ($posts as $post) {
    echo $post->title;
  }
}

Dieser Anwendungsfall wird von GraphQL auf natürliche Weise abgedeckt:

query GetPostTitles
{
  posts {
    title
  }
}

Bulk-Aktualisieren in PHP sieht so aus:

function updatePostTitlesAsUppercase()
{
  $posts = getPosts();
  foreach ($posts as $post) {
    $post->update(['title' => strtoupper($post->title)]);
  }
}

Die Ausführung von Bulk-Updates in GraphQL wird normalerweise durch das Erstellen einer dedizierten Mutation updatePosts unterstützt, die die Daten für alle Beiträge entgegennimmt.

Diesen Ansatz mag ich nicht, da er die Anzahl der Mutations im Schema effektiv verdoppelt (eine zum Mutieren der einzelnen Ressource, eine zum Mutieren mehrerer Ressourcen), und wir die Logik für beide pflegen müssen:

  • updatePost + updatePosts
  • createPost + createPosts
  • usw.

Meiner Meinung nach ist ein eleganterer Ansatz die Verwendung von verschachtelten Mutations, bei dem die Mutation Post.update auf jede der abgefragten Ressourcen angewendet wird:

mutation UpdatePostTitlesAsUppercase
{
  posts {
    title
    ucTitle: _strUpperCase(text: $__title)
    update(
      input: { title: $__ucTitle }
    ) {
      status
      post {
        title
      }
    }
  }
}

Derselbe Ansatz funktioniert für das Löschen von Ressourcen:

function deletePosts()
{
  $posts = getPosts();
  foreach ($posts as $post) {
    $post->delete();
  }
}

In GraphQL:

mutation DeletePosts
{
  posts {
    delete {
      status
    }
  }
}

Beim Erstellen übergeben wir die Ressourcen nicht, da sie noch nicht existieren; stattdessen stellen wir ein Array mit den Dateneingaben für alle zu erstellenden Ressourcen bereit:

function createPosts()
{
  $postDataItems = [
    [
      'title' => 'First title',
      'content' => 'First content',
    ],
    [
      'title' => 'Second title',
      'content' => 'Second content',
    ],
  ];
  foreach ($postDataItems as $postDataItem) {
    $post = new Post($postDataItem['title'], $postDataItem['content']);
    $post->save();
  }
}

Beiträge in GraphQL mithilfe einer einzigen createPost-Mutation in großem Umfang zu erstellen ist etwas knifflig, aber dennoch machbar.

Die Idee ist, über das Array mit den Dateneingaben zu iterieren, jeden unter einer dynamischen Variable $input zuzuweisen und dann die Mutation createPost mit diesem Input auszuführen. Schließlich erhalten wir die resultierenden IDs der erstellten Beiträge unter der dynamischen Variable $createdPostIDs und rufen ihre Daten ab:

mutation CreatePosts
  @depends(on: "GetPostsAndExportData")
{
  createdPostIDs: _echo(value: [
    {
      title: "First title",
      content: "First content"
    },
    {
      title: "Second title",
      content: "Second content"
    },
  ])
    @underEachArrayItem(
      passValueOnwardsAs: "input"
    )
      @applyField(
        name: "createPost"
        arguments: {
          input: $input
        },
        setResultInResponse: true
      )
    @export(as: "createdPostIDs")
}
 
query RetrieveCreatedPosts
  @depends(on: "CreatePosts")
{
  createdPosts: posts(
    filter: {
      ids: $createdPostIDs,
    }
  ) {
    title
    content
  }
}

Eine HTTP-Anfrage senden (und andere Funktionen)

Das Senden einer HTTP-Anfrage an einen Webserver kann über eine dedizierte Funktion in PHP wie file_get_contents oder curl_exec abgedeckt werden.

Mit file_get_contents:

$xml = file_get_contents("http://www.example.com/file.xml");

In GraphQL kann die Logik zum Ausführen einer HTTP-Anfrage über ein Funktionalitätsfeld wie _sendHTTPRequest abgedeckt werden:

query {
  _sendHTTPRequest(input: {
    url: "http://www.example.com/file.xml",
    method: GET
  }) {
    xml: body
  }
}

Dasselbe Konzept gilt für jede Funktionalität.

Zum Beispiel greifen wir auf den Wert einer Konstante in PHP so zu:

$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');

Wir können ein entsprechendes Funktionalitätsfeld in GraphQL implementieren:

{
  mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
}

Die Herausforderung nur mit GraphQL lösen

Mit all den Programmiersprachenfunktionen, die wir gerade behandelt haben, sind wir nun in der Lage, nur GraphQL zu verwenden, um das zuvor gestellte Problem zu lösen:

  • Einen Webhook erstellen, der von einem Dienst aufgerufen werden soll, wenn sich ein neuer Benutzer bei diesem Dienst anmeldet; der Benutzer hat möglicherweise den Newsletter abonniert (angegeben durch das Feld marketing_optin im Payload des Webhooks); in diesem Fall muss der Webhook die E-Mail-Adresse des Benutzers (im Feld email im Payload des Webhooks) in einer Mailchimp-Liste registrieren.

Die Lösung besteht darin, eine GraphQL persisted query als Webhook zu verwenden, mit dieser query:

query HasSubscribedToNewsletter {
  hasSubscriberOptIn: _httpRequestHasParam(name: "marketing_optin")
  subscriberOptIn: _httpRequestStringParam(name: "marketing_optin")
  isNotSubscriberOptInNAValue: _notEquals(value1: $__subscriberOptIn, value2: "NA")
  subscribedToNewsletter: _and(values: [$__hasSubscriberOptIn, $__isNotSubscriberOptInNAValue])
    @export(as: "subscribedToNewsletter")
}
 
query MaybeCreateContactOnMailchimp
   @depends(on: "HasSubscribedToNewsletter")
   @include(if: $subscribedToNewsletter)
{
  subscriberEmail: _httpRequestStringParam(name: "email")
  
  mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
   
  mailchimpPassword: _env(name: "MAILCHIMP_API_CREDENTIALS_PASSWORD")
   
  
  mailchimpListMembersJSONObject: _sendJSONObjectItemHTTPRequest(input: {
    url: "https://us7.api.mailchimp.com/3.0/lists/{listCode}/members",
    method: POST,
    options: {
      auth: {
        username: $__mailchimpUsername,
        password: $__mailchimpPassword
      },
      json: {
        email_address: $__subscriberEmail,
        status: "subscribed"
      }
    }
  })
}

In dieser Lösung wird die Operation MaybeCreateContactOnMailchimp, die die HTTP-Anfrage gegen die Mailchimp-API ausführt, bedingt ausgeführt, abhängig vom Wert des Felds marketing_optin.

(Lies den Blog-Beitrag 👨🏻‍🏫 GraphQL query to automatically send the newsletter subscribers from InstaWP to Mailchimp um zu sehen, wie diese query funktioniert.)

GraphQL ist mächtiger als du dachtest!

GraphQL kann für weit mehr verwendet werden als nur zum Abrufen und Verändern von Daten... Daten anpassen, den Output dynamisch ändern, Inhalte für verschiedene Kontexte anpassen, ein API-Gateway mit kaum ein paar Zeilen Code erstellen, und vieles mehr.

Indem wir Programmiersprachenfunktionen unterstützen, können wir die obige Herausforderung nur mit GraphQL lösen und vermeiden, einen Client bereitzustellen, der es begleitet. Wir vereinfachen damit den Anwendungs-Stack: Weniger bewegliche Teile, weniger Komplexität, weniger Code zum Debuggen, weniger Technologien im Umgang.

GraphQL rockt 🤘


Abonniere unseren Newsletter

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