Blog

👨🏻‍💻 GraphQL jako (pewien rodzaj) języka programowania

Leonardo Losoviz
Autor: Leonardo Losoviz ·

GraphQL, choć posiada język GraphQL, normalnie nie byłby nazywany językiem programowania, ponieważ jest tak wiele rzeczy, które możemy robić z językami programowania, a których nie możemy robić z GraphQL.

GraphQL jest normalnie używany do pobierania danych, na przykład do renderowania strony internetowej po stronie klienta, oraz do mutowania danych, na przykład do tworzenia posta. I na tym w zasadzie koniec.

(Inne zastosowania są po prostu kombinacjami tych 2 poprzednich przypadków. Na przykład brama API może pobierać/mutować dane z wewnętrznego serwera, który nie jest udostępniany klientowi.)

Dostęp do danych w GraphQL:

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

...ma swój (mniej więcej) odpowiednik w PHP:

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

(Wszystkie poniższe przykłady będą używać PHP jako języka programowania do porównania.)

Mutowanie danych w GraphQL:

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

...ma swój (mniej więcej) odpowiednik w PHP:

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

To jest wystarczające, ponieważ GraphQL jest normalnie używany z poziomu klienta (napisanego w jakimś języku programowania, takim jak JavaScript, PHP, Java lub innym), który zawiera logikę tego, co zrobić z danymi. Tak więc GraphQL nie jest używany samodzielnie, lecz jako uzupełnienie czegoś innego.

Ale gdyby GraphQL mógł być używany samodzielnie, wówczas wiele nowych przypadków użycia można by rozwiązać korzystając tylko z GraphQL, pozwalając na wdrożenie GraphQL w nowatorskich środowiskach i odpowiedzialność za dodatkowe zadania w stosie aplikacji.

Aby to było możliwe, GraphQL musi jednak obsługiwać wiele cech języków programowania.

Cechy języków programowania, które GraphQL obsługuje, są ograniczone. Na przykład użycie dyrektywy @include (lub @skip) i przekazanie zmiennej jako wejścia można uznać za (pewien rodzaj) logiki warunkowej:

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

To query ma swój odpowiednik w PHP:

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

I na tym w zasadzie koniec. GraphQL nie obsługuje rekurencji, zmiennych dynamicznych (gdzie ich wartości są obliczane i przypisywane do zmiennej w czasie wykonania, nie jako wejście w słowniku), przypisań zmiennych (np. przypisanie wyniku pola do zmiennej, która może być następnie dostarczona jako argument do innego pola) i innych.

Zastanów się, jak zaimplementowałbyś rozwiązanie, używając jedynie GraphQL, dla następującego problemu:

  • Utwórz webhook do wywołania przez serwis za każdym razem, gdy nowy użytkownik zarejestruje się w tym serwisie; użytkownik mógł zapisać się do newslettera (wskazane przez pole marketing_optin w ładunku webhoka); w takim przypadku webhook musi zarejestrować adres e-mail użytkownika (w polu email w ładunku webhoka) na liście Mailchimp.

Czy uważasz, że jest to wykonalne? łatwe? trudne? niemożliwe?

W Gato GraphQL, chcemy rozwiązać ten problem używając jedynie GraphQL. I wiele innych problemów. Dlatego dobrze zastanowiliśmy się nad tym, jak obsługiwać cechy języków programowania.

Przyjrzyjmy się, jakie funkcje programowania obsługujemy na naszym serwerze GraphQL. Na końcu tego posta zobaczymy, jak możemy rozwiązać ten problem.

Funkcjonalność

Pola w GraphQL normalnie dostarczają danych, takich jak tytuł, treść lub dane posta. Możemy jednak również implementować pola jako "funkcjonalność".

Na przykład wydrukowanie czasu w PHP:

function printTime()
{
  echo time();
}

...można wykonać za pomocą pola _time w GraphQL:

{
  _time
}

Zwróć uwagę, że funkcja time nie należy do żadnego typu, a zatem pole _time również nie. Jako takie jest polem globalnym i może być dostępne pod każdym typem ze schematu GraphQL:

{
  posts {
    _time
  }
}

Inne przykłady pól funkcjonalności to:

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

Funkcje

Możemy podzielić jednostki logiki na funkcje i mieć funkcję wywołującą inną funkcję:

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

W GraphQL możemy podobnie podzielić operację query (lub mutation) w dokumencie na wiele operacji query i sprawić, by operacja "zależała" od innych, wykonując je najpierw:

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"
  ])
{
  # ...
}

W tym query, wykonanie query GraphQL z przekazaniem ?operationName=PrintPostProperties do endpointu wykona najpierw queries PrintPostTitle i PrintPostContent, a dopiero potem PrintPostProperties.

Jest to możliwe dzięki Wykonywaniu Wielu Queries.

Zmienne Dynamiczne

Możemy obliczyć wartość i przypisać ją do zmiennej w czasie wykonania. Następnie, na podstawie tej wartości, możemy warunkowo wykonać pewną funkcjonalność lub nie:

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

W GraphQL możemy "eksportować" wartość pod dynamiczną zmienną w jakiejś operacji, a następnie odczytać tę wartość w innej operacji:

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

Zwróć uwagę, że zmienna $addContent, która przechowuje wartość obliczoną w czasie wykonania, jest odczytywana, ale nie zadeklarowana w operacji PrintPostProperties, ponieważ jest zmienną dynamiczną.

Warunkowe wykonywanie funkcji

Alternatywą dla poprzedniego przykładu jest grupowanie logiki w funkcje, a następnie warunkowe wykonywanie funkcji lub nie, w zależności od wartości zmiennej dynamicznej:

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;
}

W GraphQL możemy dodać dyrektywę @include do operacji:

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"
  ])
{
  # ...
}

Teraz operacja PrintPostContent zostanie wykonana tylko wtedy, gdy $addContent ma wartość true.

Przypisywanie zmiennych, podawanie ich jako wejście

Zmodyfikujmy nieznacznie poprzedni przykład, w którym warunek "addContent" był powiązany z tym, czy użytkownik był zalogowany, czy nie.

W tym innym przykładzie "addContent" ma wartość true zawsze, gdy dzisiaj jest weekend, co wymaga pewnej logiki do obliczenia:

  • Pobierz dzisiejszą datę
  • Sformatuj ją do nazwy dnia, małymi literami
  • Sprawdź, czy jest to "saturday" czy "sunday"

W 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;
  }
}

W 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)
  }
}

W operacji ExportAddContent wartość każdego odpytywanego pola jest natychmiast dostępna dla pól poniżej, pod dynamiczną zmienną $__fieldName. W ten sposób wynik pola może być natychmiast użyty jako wejście do innego pola, już w ramach tej samej operacji.

Jest to możliwe dzięki Field to Input.

Dynamiczne modyfikowanie wartości

W tym przykładzie w PHP modyfikujemy wartość zmiennej za każdym razem, gdy zalogowany użytkownik jest administratorem, w którym to przypadku do treści posta dodawany jest link do edycji posta:

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;
}

W GraphQL możemy warunkowo wykonać jedną lub drugą operację, generując różne wartości dla jakiegoś pola:

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"
  ])
{
  # ...
}

Używając dyrektyw @include i @skip z tą samą zmienną dynamiczną jako wejściem, operacje RetrieveContentForAdminUser i RetrieveContentForNonAdminUser są wzajemnie wykluczające się.

Iterowanie tablic

Powiedzmy, że chcemy iterować elementy tablicy i konwertować te wartości na wielkie litery:

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

W GraphQL możemy użyć dyrektywy @underEachArrayItem do iterowania po elementach tablicy i dostarczenia każdej z tych wartości do kolejnej dyrektywy w łańcuchu, w tym przypadku @strUpperCase:

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

Jest to możliwe dzięki dyrektywom kompozytowalnym.

Masowe operacje CRUD

CRUD oznacza Create (Tworzenie), Read (Odczyt), Update (Aktualizacja) i Delete (Usuwanie) — są to operacje stosowane na zasobach (postach, użytkownikach itp.).

Masowy odczyt w PHP wygląda następująco:

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

Ten przypadek użycia jest naturalnie obsługiwany przez GraphQL:

query GetPostTitles
{
  posts {
    title
  }
}

Masowa aktualizacja w PHP wygląda następująco:

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

Wykonywanie masowych aktualizacji w GraphQL jest normalnie obsługiwane przez tworzenie dedykowanej mutacji updatePosts, która przyjmuje dane dla wszystkich postów.

Nie podoba mi się to podejście, ponieważ efektywnie podwaja liczbę mutacji w schemacie (jedna do mutowania pojedynczego zasobu, jedna do mutowania wielu zasobów), i musimy utrzymywać logikę dla obu:

  • updatePost + updatePosts
  • createPost + createPosts
  • itd.

Moim zdaniem bardziej eleganckim podejściem jest użycie zagnieżdżonych mutacji, gdzie mutacja Post.update jest stosowana do każdego z odpytywanych zasobów:

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

To samo podejście działa przy usuwaniu zasobów:

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

W GraphQL:

mutation DeletePosts
{
  posts {
    delete {
      status
    }
  }
}

Przy tworzeniu nie przekazujemy zasobów, ponieważ jeszcze nie istnieją; zamiast tego podajemy tablicę z danymi wejściowymi dla wszystkich zasobów do utworzenia:

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();
  }
}

Tworzenie postów masowo w GraphQL przy użyciu jednej mutacji createPost jest nieco skomplikowane, ale jest wykonalne.

Pomysł polega na iterowaniu po tablicy z danymi wejściowymi, przypisaniu każdego elementu pod dynamiczną zmienną $input, a następnie wykonaniu mutacji createPost przekazując to wejście. Na koniec pobieramy wynikowe identyfikatory utworzonych postów pod dynamiczną zmienną $createdPostIDs i pobieramy ich dane:

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
  }
}

Wysyłanie żądania HTTP (i inne funkcje)

Wysyłanie żądania HTTP do jakiegoś serwera może być zrealizowane poprzez dedykowaną funkcję w PHP, taką jak file_get_contents lub curl_exec.

Używając file_get_contents:

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

W GraphQL logika wykonywania żądania HTTP może być zrealizowana poprzez pole funkcjonalności, takie jak _sendHTTPRequest:

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

To samo pojęcie dotyczy każdej funkcjonalności.

Na przykład dostęp do wartości stałej w PHP wygląda tak:

$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');

Możemy zaimplementować odpowiednie pole funkcjonalności w GraphQL:

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

Rozwiązywanie wyzwania używając tylko GraphQL

Dzięki wszystkim cechom języków programowania, które właśnie omówiliśmy, możemy teraz użyć tylko GraphQL do rozwiązania problemu postawionego wcześniej:

  • Utwórz webhook do wywołania przez serwis za każdym razem, gdy nowy użytkownik zarejestruje się w tym serwisie; użytkownik mógł zapisać się do newslettera (wskazane przez pole marketing_optin w ładunku webhoka); w takim przypadku webhook musi zarejestrować adres e-mail użytkownika (w polu email w ładunku webhoka) na liście Mailchimp.

Rozwiązanie polega na użyciu utrwalonego query GraphQL jako webhoka z tym 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"
      }
    }
  })
}

W tym rozwiązaniu operacja MaybeCreateContactOnMailchimp, która wykonuje żądanie HTTP do API Mailchimp, zostanie wykonana warunkowo, w zależności od wartości pola marketing_optin.

(Przeczytaj post na blogu 👨🏻‍🏫 Query GraphQL do automatycznego wysyłania subskrybentów newslettera z InstaWP do Mailchimp, aby zobaczyć, jak działa to query.)

GraphQL jest potężniejszy niż myślałeś!

GraphQL może być używany do znacznie więcej niż tylko pobierania i mutowania danych... Adaptacja danych, dynamiczne modyfikowanie wyniku, dostosowywanie treści do różnych kontekstów, tworzenie bramy API zaledwie kilkoma liniami kodu i wiele innych.

Obsługując cechy języków programowania, możemy rozwiązać powyższe wyzwanie używając tylko GraphQL i unikać wdrażania klienta jako towarzysza. Upraszczamy w ten sposób stos aplikacji: mniej ruchomych części, mniej złożoności, mniej kodu do debugowania, mniej technologii do obsługi.

GraphQL jest niesamowity 🤘


Zapisz się do naszego newslettera

Bądź na bieżąco ze wszystkimi aktualizacjami Gato GraphQL.