Blog

🤔 Dlaczego wydanie nowego Gato GraphQL zajęło 1,5 roku?

Leonardo Losoviz
Autor: Leonardo Losoviz ·

Wersja 0.9 Gato GraphQL właśnie została wydana. Wymagało to prawie 1,5 roku rozwoju i ponad 16 000 commitów, aby być gotowa. To naprawdę długo!

Po podzieleniu się ogłoszeniem na Hacker News, otrzymałem następujące pytanie:

[...] Jestem ciekawy, co wymagało 16k commitów. Projekty, w których uczestniczyłem z ponad dziesięcioma tysiącami commitów, miały wiele dziesiątek lub setek osób pracujących na pełen etat. [...] Czy istnieje jakaś złożoność, którą trzeba było pokonać, a której post nie porusza?

Liczba commitów nie jest zbyt wiarygodną metryką, ponieważ mogę wprowadzić bardzo prostą zmianę i opublikować ją jako pojedynczy commit. Wiele z tych 16k commitów to commity "typo", lub po prostu poprawiony opis w jakimś README.

Niemniej jednak liczba commitów daje pewne pojęcie o rzeczywistym wysiłku. Było też wiele commitów wypełnionych modyfikacjami, obejmującymi dziesiątki, a nawet setki zmian na raz. Zmiany między wersjami 0.8 a 0.9 są naprawdę ogromne, i wymagało to wysiłku i czasu, aby je zrealizować.

W tym wpisie na blogu opiszę, jakie to zmiany, aby wyjaśnić, dlaczego zajęło to tak długo. I robiąc to, dam również podgląd niektórych zaawansowanych funkcji, które zostały dodane do bazy kodu i które ujrzą światło dzienne wraz z nadchodzącą wersją 1.0.

Kontekst serwera GraphQL

Najpierw podzielę się trochę historią silnika i szczegółami technicznymi dotyczącymi jego działania.

(Jest to głównie istotne dla deweloperów; jeśli nie interesują Cię kwestie techniczne, zapraszam do przejścia do następnej sekcji.)

Gato GraphQL jest oparty na PoP, silniku renderującym komponenty w PHP (podobnie jak React czy Vue w JavaScript). Jego zależność od tego silnika jest absolutna, dlatego plugin jest hostowany w monorepo GatoGraphQL/GatoGraphQL na GitHub.

Pod maską ta zależność wygląda następująco:

Gato GraphQL rozwiązuje query GraphQL poprzez jej przekształcenie na równoważny model komponentów, który PoP rozwiązuje pobierając wszystkie wymagane dane, a następnie te dane przyjmują kształt query GraphQL.

Kiedy zacząłem pracować nad PoP gdzieś około 2013/2014, nie było GraphQL, a metodologia rozwiązywania modelu komponentów w dane została zaprojektowana i zaimplementowana od podstaw. Brak modelu do naśladowania (takiego jak GraphQL dla koncepcji i projekt referencyjny graphql-js dla implementacji) był zarówno przeszkodą, jak i błogosławieństwem, jak wyjaśnię później.

PoP był początkowo zaprojektowany do renderowania całej witryny jako HTML po stronie serwera, jednocześnie udostępniając surowe dane w formacie JSON po dodaniu ?output=json do adresu URL strony, i dalej wybierając, jakie dane pobierać (ustawienia, dane obiektów DB) za pomocą dodatkowych parametrów URL.

Kliknij poniższe linki (wszystkie wskazują tę samą stronę, tylko z różnymi parametrami URL) i zauważ, jak się różnią:

Po kliknięciu na ostatni link przychodzi pewne olśnienie: to jest praktycznie GraphQL! Jedyna duża różnica polega na tym, że dane w odpowiedzi są niejawne, ponieważ zostały już zdefiniowane przez komponenty (w PHP) dołączone do strony. GraphQL natomiast pozwala nam zdecydować, jakie dane pobierać za pomocą query.

Kiedy więc dowiedziałem się o GraphQL około 2019 roku, dla mnie było oczywiste, żeby PoP obsługiwał również serwer GraphQL. Wystarczyło, że akceptował query GraphQL jako dane wejściowe i dynamicznie tworzył model komponentów na podstawie tej query.

I właśnie to zrobiłem. I działało dobrze. Ale było wolne, ponieważ PoP rozumiał własny format wejściowy, więc query GraphQL musiało być dostosowane do formatu PoP:

  1. Przetworzyć query GraphQL; następnie
  2. Przekształcić query do formatu PoP; następnie
  3. Przetworzyć format PoP

Przetwarzanie query GraphQL było wtedy wykonywane dwukrotnie (raz dla GraphQL, raz dla PoP), a format PoP nie był rozwiązywany za pomocą AST, lecz jedynie przez wielokrotne parsowanie ciągu query. (Niestosowanie AST było strasznym kodowaniem, ale nie miałem specyfikacji do śledzenia, a jego rozwój przebiegał organicznie, gdzie proste substr(...) ratowało sytuację każdego dnia.)

Dlatego mówię, że brak specyfikacji GraphQL był przeszkodą, ponieważ moje rozwiązanie było wolne (i taka była sytuacja w wersji 0.8). Zdecydowałem więc to naprawić.

Przekształcanie silnika w GraphQL-first

Rozwiązanie, które wybrałem, polega na tym, że PoP będzie natywnie mówił językiem GraphQL. Wtedy przekazanie query GraphQL do PoP jako danych wejściowych byłoby już konwertowane do modelu komponentów, bez potrzeby żadnego dodatkowego adaptera ani robienia rzeczy dwa razy.

Oznaczało to, że projekt PoP musiał zostać przekształcony — z biblioteki PHP renderującej komponenty dla witryn po stronie serwera dostosowanej do rozwiązywania queries GraphQL, w faktyczny serwer GraphQL.

Baza kodu przeszła wtedy masową transformację, wprowadzając GraphQL AST jako fundament do komunikowania stanu pomiędzy wszystkimi usługami PHP w silniku. Obiekty GraphQL AST są teraz danymi wejściowymi dla PoP (zamiast ciągów query).

Inne serwery GraphQL w PHP opierają się na graphql-php, ale plugin Gato GraphQL nie. To zła wiadomość dotycząca wysiłku związanego z utrzymaniem (bo nie mogę ponownie wykorzystać kodu napisanego przez kogoś innego), ale dobra wiadomość dotycząca niezależności: mogę decydować o dodawaniu niestandardowych funkcji do mojego pluginu we własnym tempie i zgodnie z własnym kryterium (dlatego plugin już udostępnia input object "oneof").

I jak zostanie pokazane w poniższej sekcji, to jest ogromną zaletą.

Wprowadzanie oryginalnych funkcji do GraphQL

GraphQL jest zazwyczaj kojarzony z pobieraniem danych. Oczywiście możesz pobierać dowolne dane (posty, użytkowników, komentarze itp.) z Gato GraphQL:

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

Ale to jest nisko wiszący owoc. GraphQL może być również używany do wielu innych przypadków użycia, w tym manipulacji i transformacji danych, a nawet umieszczenia GraphQL w potoku do pośredniczenia między usługami.

Oto kilka przykładów, gdzie GraphQL jest przydatny:

  • Wyodrębnianie informacji z jednego lub więcej źródeł (takich jak użytkownicy z witryn WordPress i dane kontaktowe newslettera z Mailchimp), łączenie danych i analizowanie ich wszystkich razem jako jednego zbioru danych
  • Wykonywanie operacji w celu dostosowania treści na stronie:
    • Jednorazowo, jak przy migracji witryny do innej domeny i zastępowaniu "www.myoldsite.com" przez "mynewsite.com" wszędzie w treści i metadanych
    • Ciągłe, jak zastępowanie każdego "http://" przez "https://" za każdym razem, gdy pisarz opublikuje nowy wpis na blogu
  • Łączenie się z API Google Translate w celu przetłumaczenia wszystkich wpisów na blogu na inny język
  • Automatyczne wysyłanie tweeta po opublikowaniu wpisu na blogu

PoP został zaprojektowany do obsługi tych innych przypadków użycia, za pomocą funkcji, które nie są (naturalnie) obsługiwane przez GraphQL, takich jak:

  • Obsługa pól "funkcjonalności" (oprócz pól "danych"), które są dodawane do wszystkich typów w schemacie
  • Przekazywanie wyniku pola jako danych wejściowych do innego pola, w ramach tej samej query
  • Komponowanie dyrektyw, aby jedna dyrektywa modyfikowała zachowanie innej dyrektywy
  • Dynamiczne decydowanie, czy zastosować dyrektywę, na podstawie wartości pola

I z pewnością nie chciałem usuwać tych funkcji z serwera GraphQL: już je zakodowałem, i są z pewnością wartościowe.

Dlatego drugim powodem, dla którego v0.9 zajęła tak długo, jest to, że musiałem również znaleźć sposób na włączenie tych nowych możliwości do GraphQL, w sposób, który nie naruszałby specyfikacji GraphQL (na przykład, wprowadzanie nowych elementów do składni GraphQL było wykluczone).

Przykład manipulacji danymi w GraphQL

Nowe możliwości wprowadzone do GraphQL w pluginie staną się bardziej widoczne w niedalekiej przyszłości, gdy zostanie wydana wersja 1.0. Ale już teraz możesz spróbować niektórych z nich.

Poniższe query GraphQL pobiera listę wpisów użytkowników z zewnętrznego API REST (które można @removeować z odpowiedzi); wprowadza te dane do innego pola, w ramach tej samej query; wyodrębnia właściwość email z każdego wpisu; i na koniec przekształca email na wielkie litery, ale tylko jeśli język w tym samym wpisie to angielski lub niemiecki:

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

To jest odpowiedź (zwróć uwagę, jak tylko niektóre adresy email zostały zamienione na wielkie litery):

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

Sprawdź to sam! Naciśnij przycisk "Run", aby wykonać query:

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

Wspomniałem, że brak kierowania się specyfikacją GraphQL był przeszkodą, ale (z perspektywy czasu) również błogosławieństwem. Wynika to z faktu, że nie miałem ograniczeń specyfikacji GraphQL, więc mogłem sobie pozwolić na marzenie o tych nowych możliwościach.

A teraz, gdy te funkcje zostały przeniesione do Gato GraphQL, może on być niezwykle użytecznym sojusznikiem we wszystkim, co związane z pobieraniem, manipulacją i transformacją treści dla Twojej witryny WordPress. (Choć będą one dostępne dopiero z nadchodzącą v1.0).

Zajęło to chwilę, ale wysiłek był zdecydowanie tego wart.

Wypróbuj to!

Czy jesteś przekonany, że długie oczekiwanie było warte? Mam nadzieję!

Śmiało, pobierz plugin i sprawdź go:

Chcesz otrzymywać aktualności dotyczące jego rozwoju, nową dokumentację i nadchodzące wydania, w tym v1.0? Zapraszamy do subskrypcji newslettera.

Chcesz poznać kod open source na GitHub? Sprawdź GatoGraphQL/GatoGraphQL (i zapraszamy do dawania gwiazdki... Uwielbiamy gwiazdki! ⭐️⭐️⭐️)

Nawiasem mówiąc, jakich transformacji treści potrzebujesz w WordPress (do których być może już używasz jakiegoś dedykowanego komercyjnego pluginu)? Proszę, wyślij mi wiadomość z opisem swojego przypadku użycia.

Jeśli podoba Ci się to, co widzisz, podziel się z przyjaciółmi i kolegami, pomóż szerzyć miłość ❤️.


Zapisz się do naszego newslettera

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